Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cold-lands-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"stack-effect": patch
---

fix strip cross-target module from autoselect
11 changes: 11 additions & 0 deletions .changeset/lazy-corners-win.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"stack-effect": minor
---

add new Toolkits for the ai module

- **DateTimeToolkit** <- provides functions for working with dates and times
- **MathToolkit** <- provides functions for mathematical operations
- **MemoryToolkit** <- provides functions for working with memory and data storage
- **PlanToolkit** <- provides functions for creating and managing plans and tasks
- **WebFetchToolkit** <- provides functions for making web requests and fetching data from the internet
100 changes: 94 additions & 6 deletions apps/cli/src/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ const buildModuleTree = (
Effect.gen(function* () {
const catalog = yield* CatalogService;

// Collect all module IDs that appear as children of other modules
// so they are excluded from the top-level list (they appear nested instead)
const childModuleIds = new Set(
Arr.flatMap(modules, (mod) =>
Arr.map(mod.children ?? [], (child) => child.moduleId),
),
);

// Build tree node recursively, following dependencies and implies
// visited is a Ref for mutable tracking across sibling branches
const buildNode = (
Expand Down Expand Up @@ -131,7 +139,9 @@ const buildModuleTree = (
// Mark as visited before processing children
yield* Ref.update(visitedRef, (s) => new Set([...s, mod.id]));

// Process required dependencies
// Process required dependencies (cross-target required-module deps)
// These are included in the tree for visibility, but are filtered out
// when building the Selection (BlueprintService resolves them automatically).
const depChildren = yield* pipe(
mod.dependencies,
Arr.filter(
Expand Down Expand Up @@ -242,7 +252,12 @@ const buildModuleTree = (
});

// Build tree for each top-level module (each with fresh visited Ref)
return yield* Effect.forEach(modules, (mod) =>
// Exclude modules that are children of other modules in this set
const topLevelModules = Arr.filter(
modules,
(mod) => !childModuleIds.has(mod.id),
);
return yield* Effect.forEach(topLevelModules, (mod) =>
Effect.gen(function* () {
const visitedRef = yield* Ref.make(new Set<string>());
const result = yield* buildNode(mod, "root", visitedRef);
Expand Down Expand Up @@ -817,6 +832,7 @@ export const add = Command.make(
Effect.gen(function* () {
const configure = yield* ConfigureService;
const pipeline = yield* ScaffoldPipeline;
const catalog = yield* CatalogService;

const repoRoot = Option.getOrElse(flags.root, () => process.cwd());

Expand All @@ -842,11 +858,83 @@ export const add = Command.make(
)
: yield* collectTargetsInteractive;

// Build selection
// Build selection, routing each module to the target it's supported on.
// Cross-target module IDs that appeared in the TUI tree (e.g., toolkit
// children of a parent on another target) are placed on the correct target.
// BlueprintService will also resolve required-module deps automatically.
const selectionTargets = new Map<
string,
{ identity: TargetIdentity; modules: Set<string> }
>();

// Seed with collected targets
for (const t of collected) {
const identity = new TargetIdentity({ kind: t.kind, name: t.name });
selectionTargets.set(identity.toKey(), {
identity,
modules: new Set(),
});
}

// Route each module to its supported target
yield* Effect.forEach(collected, (t) =>
Effect.gen(function* () {
const identity = new TargetIdentity({ kind: t.kind, name: t.name });
yield* Effect.forEach(Arr.dedupe(t.modules), (id) =>
Effect.gen(function* () {
// Check if supported on the collected target first
const ownSupported = yield* catalog
.isSupportedOn(id, identity)
.pipe(Effect.orElseSucceed(() => false));
if (ownSupported) {
selectionTargets.get(identity.toKey())!.modules.add(id);
return;
}

// Find which target this module belongs to
const mod = yield* catalog
.getModule(id)
.pipe(Effect.orElseSucceed(() => null));
if (!mod) return;

for (const rule of mod.supportedOn) {
if (rule._tag === "identity") {
const targetKey = new TargetIdentity(rule.identity).toKey();
if (!selectionTargets.has(targetKey)) {
selectionTargets.set(targetKey, {
identity: new TargetIdentity(rule.identity),
modules: new Set(),
});
}
selectionTargets.get(targetKey)!.modules.add(id);
return;
}
if (rule._tag === "kind") {
// Find existing target of this kind
const existing = Arr.findFirst(collected, (c) =>
c.kind === rule.kind ? true : false,
);
if (Option.isSome(existing)) {
const key = new TargetIdentity({
kind: existing.value.kind,
name: existing.value.name,
}).toKey();
selectionTargets.get(key)!.modules.add(id);
return;
}
}
}
}),
);
}),
);

const selection: typeof Selection.Type = {
targets: Arr.map(collected, (t) => ({
identity: new TargetIdentity({ kind: t.kind, name: t.name }),
modules: Arr.map(t.modules, (id) => ({ id })),
targets: Arr.fromIterable(selectionTargets.values()).map((entry) => ({
identity: entry.identity,
modules: Arr.fromIterable(entry.modules).map((id) => ({
id: id as typeof ModuleId.Type,
})),
})),
};

Expand Down
Loading
Loading