@evandu/super-pi 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/client/assets/index-BsQxHODf.js +43 -0
  2. package/dist/client/assets/{index-WlIFA_Dt.css → index-DP3T9VzH.css} +1 -1
  3. package/dist/client/index.html +2 -2
  4. package/dist/server/agents.d.ts +20 -1
  5. package/dist/server/agents.js +92 -18
  6. package/dist/server/application/catalog-service.d.ts +22 -2
  7. package/dist/server/application/catalog-service.js +61 -9
  8. package/dist/server/application/conversation-service.d.ts +3 -2
  9. package/dist/server/application/conversation-service.js +5 -1
  10. package/dist/server/application/run-service.d.ts +3 -2
  11. package/dist/server/application/run-service.js +3 -0
  12. package/dist/server/application/workspace-service.d.ts +3 -2
  13. package/dist/server/application/workspace-service.js +5 -1
  14. package/dist/server/command-handler.js +8 -0
  15. package/dist/server/http-server.js +3 -1
  16. package/dist/server/index.js +27 -10
  17. package/dist/server/runtime.d.ts +5 -2
  18. package/dist/server/runtime.js +6 -2
  19. package/dist/server/server-context.d.ts +1 -0
  20. package/dist/server/store.d.ts +6 -0
  21. package/dist/server/store.js +16 -0
  22. package/dist/server/subagent-runs.d.ts +2 -0
  23. package/dist/server/subagent-runs.js +4 -0
  24. package/dist/server/subagents.d.ts +3 -2
  25. package/dist/server/subagents.js +5 -1
  26. package/dist/server/types.d.ts +19 -0
  27. package/dist/server/version.d.ts +10 -0
  28. package/dist/server/version.js +39 -0
  29. package/dist/server/ws-server.d.ts +2 -0
  30. package/dist/server/ws-server.js +12 -0
  31. package/package.json +4 -2
  32. package/dist/client/assets/index-CHw4ZoY4.js +0 -43
@@ -1,14 +1,34 @@
1
1
  import type { AgentRuntime } from "../runtime.js";
2
2
  import type { AgentCatalog, ModelCatalog, ModelSelection } from "../types.js";
3
3
  import type { EventBroadcaster } from "../server-context.js";
4
+ import type { Store } from "../store.js";
4
5
  /**
5
6
  * Service managing model catalogs and agent definitions (primary and subagents).
6
7
  */
7
8
  export declare class CatalogService {
8
9
  private readonly runtime;
9
10
  private readonly broadcaster;
10
- private readonly agentsDir;
11
- constructor(runtime: AgentRuntime, broadcaster: EventBroadcaster, agentsDir: string);
11
+ private readonly store?;
12
+ private readonly rootDir?;
13
+ private readonly onDirsChanged?;
14
+ private agentDirs;
15
+ constructor(runtime: AgentRuntime, broadcaster: EventBroadcaster, agentsDir: string | readonly string[], store?: Store | undefined, rootDir?: string | undefined, onDirsChanged?: ((dirs: string[]) => void) | undefined);
16
+ /**
17
+ * Returns current active agent discovery directories.
18
+ */
19
+ getAgentDirs(): string[];
20
+ /**
21
+ * Updates agent discovery directories and notifies subscribers.
22
+ */
23
+ setAgentDirs(dirs: string[]): void;
24
+ /**
25
+ * Updates user-configured custom agent directories in store and triggers rescan.
26
+ */
27
+ setCustomDirs(dirs: string[]): Promise<void>;
28
+ /**
29
+ * Immediately re-reads agent definitions from disk and broadcasts catalogs.
30
+ */
31
+ rescan(): Promise<void>;
12
32
  /**
13
33
  * Fetches the model catalog from runtime, optionally tailored to a selected model.
14
34
  */
@@ -1,4 +1,4 @@
1
- import { readAgents } from "../agents.js";
1
+ import { readAgents, resolveAgentDirectories } from "../agents.js";
2
2
  function summarize(agent) {
3
3
  const { prompt, toolFlags, ...summary } = agent;
4
4
  const toolSummary = toolFlags.includes("--no-tools")
@@ -16,11 +16,54 @@ function summarize(agent) {
16
16
  export class CatalogService {
17
17
  runtime;
18
18
  broadcaster;
19
- agentsDir;
20
- constructor(runtime, broadcaster, agentsDir) {
19
+ store;
20
+ rootDir;
21
+ onDirsChanged;
22
+ agentDirs;
23
+ constructor(runtime, broadcaster, agentsDir, store, rootDir, onDirsChanged) {
21
24
  this.runtime = runtime;
22
25
  this.broadcaster = broadcaster;
23
- this.agentsDir = agentsDir;
26
+ this.store = store;
27
+ this.rootDir = rootDir;
28
+ this.onDirsChanged = onDirsChanged;
29
+ this.agentDirs = Array.isArray(agentsDir) ? [...agentsDir] : [agentsDir];
30
+ }
31
+ /**
32
+ * Returns current active agent discovery directories.
33
+ */
34
+ getAgentDirs() {
35
+ return [...this.agentDirs];
36
+ }
37
+ /**
38
+ * Updates agent discovery directories and notifies subscribers.
39
+ */
40
+ setAgentDirs(dirs) {
41
+ this.agentDirs = [...dirs];
42
+ this.onDirsChanged?.(this.agentDirs);
43
+ }
44
+ /**
45
+ * Updates user-configured custom agent directories in store and triggers rescan.
46
+ */
47
+ async setCustomDirs(dirs) {
48
+ if (this.store) {
49
+ await this.store.setCustomAgentDirs(dirs);
50
+ this.broadcaster.broadcastState();
51
+ }
52
+ const resolved = resolveAgentDirectories({
53
+ customDirs: dirs,
54
+ rootDir: this.rootDir,
55
+ });
56
+ this.setAgentDirs(resolved);
57
+ await this.rescan();
58
+ }
59
+ /**
60
+ * Immediately re-reads agent definitions from disk and broadcasts catalogs.
61
+ */
62
+ async rescan() {
63
+ await Promise.all([
64
+ this.broadcastAgents(),
65
+ this.broadcastSubagents(),
66
+ ]);
24
67
  }
25
68
  /**
26
69
  * Fetches the model catalog from runtime, optionally tailored to a selected model.
@@ -39,23 +82,32 @@ export class CatalogService {
39
82
  * Reads one catalog filtered by mode at source.
40
83
  */
41
84
  async readAgentCatalog(mode) {
85
+ const primaryDir = this.agentDirs[0] ?? "";
42
86
  try {
43
- const scan = await readAgents(this.agentsDir, { mode });
87
+ const scan = await readAgents(this.agentDirs, { mode });
44
88
  for (const problem of scan.problems) {
45
- console.warn(`Super Pi: ${this.agentsDir}/${problem.file} is not a usable agent - ${problem.message}`);
89
+ console.warn(`Super Pi: ${problem.file} is not a usable agent - ${problem.message}`);
46
90
  }
47
91
  const agents = scan.agents.filter((agent) => !agent.hidden).map(summarize);
48
92
  if (!agents.length) {
49
- console.warn(`Super Pi: no ${mode} agents found in ${this.agentsDir} (${scan.problems.length} file(s) rejected).`);
93
+ console.warn(`Super Pi: no ${mode} agents found in [${this.agentDirs.join(", ")}] (${scan.problems.length} file(s) rejected).`);
50
94
  }
51
- return { source: "directory", agents, problems: scan.problems, directory: this.agentsDir, fetchedAt: new Date().toISOString() };
95
+ return {
96
+ source: "directory",
97
+ agents,
98
+ problems: scan.problems,
99
+ directory: primaryDir,
100
+ directories: [...this.agentDirs],
101
+ fetchedAt: new Date().toISOString(),
102
+ };
52
103
  }
53
104
  catch (error) {
54
105
  return {
55
106
  source: "unavailable",
56
107
  agents: [],
57
108
  problems: [],
58
- directory: this.agentsDir,
109
+ directory: primaryDir,
110
+ directories: [...this.agentDirs],
59
111
  fetchedAt: new Date().toISOString(),
60
112
  error: error instanceof Error ? error.message : "unknown error",
61
113
  };
@@ -27,8 +27,9 @@ export declare class ConversationService {
27
27
  private readonly catalogService;
28
28
  private readonly runService;
29
29
  private readonly extensionUiService;
30
- private readonly agentsDir;
31
- constructor(store: Store, runtime: AgentRuntime, subagents: SubagentRunRecorder, broadcaster: EventBroadcaster, catalogService: CatalogService, runService: RunService, extensionUiService: ExtensionUiService, agentsDir: string);
30
+ private agentsDir;
31
+ constructor(store: Store, runtime: AgentRuntime, subagents: SubagentRunRecorder, broadcaster: EventBroadcaster, catalogService: CatalogService, runService: RunService, extensionUiService: ExtensionUiService, agentsDir: string | string[]);
32
+ setAgentsDir(dir: string | string[]): void;
32
33
  /**
33
34
  * Creates a new conversation in a workspace.
34
35
  */
@@ -22,6 +22,9 @@ export class ConversationService {
22
22
  this.extensionUiService = extensionUiService;
23
23
  this.agentsDir = agentsDir;
24
24
  }
25
+ setAgentsDir(dir) {
26
+ this.agentsDir = dir;
27
+ }
25
28
  /**
26
29
  * Creates a new conversation in a workspace.
27
30
  */
@@ -101,7 +104,8 @@ export class ConversationService {
101
104
  }
102
105
  const agent = await this.runtime.agentFor?.({ id: conversation.id, agentId });
103
106
  if (!agent) {
104
- throw new Error(`Agent ${JSON.stringify(agentId)} 不存在或不是可用的 primary agent(目录:${this.agentsDir})`);
107
+ const dirsLabel = Array.isArray(this.agentsDir) ? this.agentsDir.join(", ") : this.agentsDir;
108
+ throw new Error(`Agent ${JSON.stringify(agentId)} 不存在或不是可用的 primary agent(目录:${dirsLabel})`);
105
109
  }
106
110
  if (agent.model) {
107
111
  const catalog = await this.runtime.catalog();
@@ -43,10 +43,11 @@ export declare class RunService {
43
43
  private readonly subagents;
44
44
  private readonly broadcaster;
45
45
  private readonly extensionUiService;
46
- private readonly agentsDir;
46
+ private agentsDir;
47
47
  private readonly workflows;
48
48
  private readonly activeRuns;
49
- constructor(store: Store, runtime: AgentRuntime, subagents: SubagentRunRecorder, broadcaster: EventBroadcaster, extensionUiService: ExtensionUiService, agentsDir: string, workflows: WorkflowEngine);
49
+ constructor(store: Store, runtime: AgentRuntime, subagents: SubagentRunRecorder, broadcaster: EventBroadcaster, extensionUiService: ExtensionUiService, agentsDir: string | string[], workflows: WorkflowEngine);
50
+ setAgentsDir(dir: string | string[]): void;
50
51
  /**
51
52
  * Resolves the conversation and workspace synchronously for a run.
52
53
  */
@@ -25,6 +25,9 @@ export class RunService {
25
25
  this.agentsDir = agentsDir;
26
26
  this.workflows = workflows;
27
27
  }
28
+ setAgentsDir(dir) {
29
+ this.agentsDir = dir;
30
+ }
28
31
  /**
29
32
  * Resolves the conversation and workspace synchronously for a run.
30
33
  */
@@ -23,8 +23,9 @@ export declare class WorkspaceService {
23
23
  private readonly runService;
24
24
  private readonly extensionUiService;
25
25
  private readonly uploadsDir;
26
- private readonly agentsDir;
27
- constructor(store: Store, runtime: AgentRuntime, broadcaster: EventBroadcaster, catalogService: CatalogService, runService: RunService, extensionUiService: ExtensionUiService, uploadsDir: string, agentsDir: string);
26
+ private agentsDir;
27
+ constructor(store: Store, runtime: AgentRuntime, broadcaster: EventBroadcaster, catalogService: CatalogService, runService: RunService, extensionUiService: ExtensionUiService, uploadsDir: string, agentsDir: string | string[]);
28
+ setAgentsDir(dir: string | string[]): void;
28
29
  /**
29
30
  * Creates a new workspace.
30
31
  */
@@ -21,6 +21,9 @@ export class WorkspaceService {
21
21
  this.uploadsDir = uploadsDir;
22
22
  this.agentsDir = agentsDir;
23
23
  }
24
+ setAgentsDir(dir) {
25
+ this.agentsDir = dir;
26
+ }
24
27
  /**
25
28
  * Creates a new workspace.
26
29
  */
@@ -91,7 +94,8 @@ export class WorkspaceService {
91
94
  }
92
95
  const agent = await this.runtime.agentFor({ id: workspaceId, agentId: settings.agentId });
93
96
  if (!agent) {
94
- throw new Error(`Agent ${JSON.stringify(settings.agentId)} 不存在或不是可用的 primary agent(目录:${this.agentsDir})`);
97
+ const dirsLabel = Array.isArray(this.agentsDir) ? this.agentsDir.join(", ") : this.agentsDir;
98
+ throw new Error(`Agent ${JSON.stringify(settings.agentId)} 不存在或不是可用的 primary agent(目录:${dirsLabel})`);
95
99
  }
96
100
  agentToStore = agent.id;
97
101
  }
@@ -136,6 +136,14 @@ export class CommandHandler {
136
136
  await this.catalogService.broadcastSubagents();
137
137
  return;
138
138
  }
139
+ if (command.type === "agents.setCustomDirs") {
140
+ await this.catalogService.setCustomDirs(command.dirs);
141
+ return;
142
+ }
143
+ if (command.type === "agents.rescan") {
144
+ await this.catalogService.rescan();
145
+ return;
146
+ }
139
147
  throw new Error(`未知的命令类型:${JSON.stringify(command.type ?? null)}`);
140
148
  }
141
149
  }
@@ -4,6 +4,7 @@ import { createServer } from "node:http";
4
4
  import { basename, dirname, extname, join, relative, resolve } from "node:path";
5
5
  import { describe as describeFilesError, fileMeta, listDirectory, MAX_FILE_BYTES, readWorkspaceFile, resolveInsideRoot, searchByName, toRelative, writeWorkspaceFile, } from "./files.js";
6
6
  import { findUploadFile, MAX_UPLOAD_BODY_BYTES, saveUpload } from "./upload.js";
7
+ import { checkDiskVersion } from "./version.js";
7
8
  const MIME_TYPES = {
8
9
  ".css": "text/css; charset=utf-8",
9
10
  ".html": "text/html; charset=utf-8",
@@ -51,8 +52,9 @@ export function createHttpServer(options) {
51
52
  return;
52
53
  }
53
54
  if (url.pathname === "/api/health") {
55
+ const version = checkDiskVersion(rootDir);
54
56
  response.writeHead(200, { "content-type": "application/json; charset=utf-8" })
55
- .end(JSON.stringify({ status: "ok", service: "super-pi" }));
57
+ .end(JSON.stringify({ status: "ok", service: "super-pi", ...version }));
56
58
  return;
57
59
  }
58
60
  if (url.pathname === "/api/subagent/dispatch") {
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
4
4
  import { join, resolve } from "node:path";
5
5
  import { defaultStorePath, Store } from "./store.js";
6
6
  import { PiRpcRuntime, WelcomeRuntime } from "./runtime.js";
7
+ import { resolveAgentDirectories } from "./agents.js";
7
8
  import { releaseAllCapabilities } from "./capability.js";
8
9
  import { SubagentSupervisor } from "./subagents.js";
9
10
  import { SubagentRunRecorder } from "./subagent-runs.js";
@@ -16,15 +17,16 @@ import { WorkspaceService } from "./application/workspace-service.js";
16
17
  import { CommandHandler } from "./command-handler.js";
17
18
  import { createHttpServer } from "./http-server.js";
18
19
  import { WsBroadcaster, WsServer } from "./ws-server.js";
20
+ import { initRunningVersion } from "./version.js";
19
21
  // ------------------------------------------------------------- Environment & paths
20
22
  const rootDir = resolve(import.meta.dirname, "../..");
23
+ initRunningVersion(rootDir);
21
24
  const port = Number.parseInt(process.env.SUPER_PI_PORT ?? "3080", 10);
22
25
  const host = process.env.SUPER_PI_HOST ?? "127.0.0.1";
23
26
  const defaultDataDir = !rootDir.includes("node_modules") && existsSync(join(rootDir, ".super-pi"))
24
27
  ? join(rootDir, ".super-pi")
25
28
  : join(homedir(), ".super-pi");
26
29
  const dataDir = resolve(process.env.SUPER_PI_DATA_DIR ?? defaultDataDir);
27
- const agentsDir = resolve(process.env.SUPER_PI_AGENTS_DIR ?? join(rootDir, "agents"));
28
30
  const agentPromptDir = join(dataDir, "agent-prompts");
29
31
  const clientDir = join(rootDir, "dist", "client");
30
32
  const accessToken = process.env.SUPER_PI_ACCESS_TOKEN;
@@ -33,23 +35,29 @@ await mkdir(uploadsDir, { recursive: true }).catch(() => { });
33
35
  const taskEndpoint = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}/api/subagent/dispatch`;
34
36
  const todoEndpoint = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}/api/todos`;
35
37
  const workflowEndpoint = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${port}/api/workflow/run`;
38
+ // ------------------------------------------------------------- Core singletons
39
+ const store = await Store.open(defaultStorePath(dataDir));
40
+ const initialAgentDirs = resolveAgentDirectories({
41
+ customDirs: store.snapshot().customAgentDirs,
42
+ rootDir,
43
+ });
44
+ const agentsDir = initialAgentDirs[0] ?? resolve(process.env.SUPER_PI_AGENTS_DIR ?? join(rootDir, "agents"));
36
45
  const paths = {
37
46
  dataDir,
38
47
  rootDir,
39
48
  agentsDir,
49
+ agentDirs: initialAgentDirs,
40
50
  agentPromptDir,
41
51
  uploadsDir,
42
52
  clientDir,
43
53
  };
44
- // ------------------------------------------------------------- Core singletons
45
- const store = await Store.open(defaultStorePath(dataDir));
46
54
  const runtime = process.env.SUPER_PI_RUNTIME === "welcome"
47
55
  ? new WelcomeRuntime()
48
- : new PiRpcRuntime(join(dataDir, "pi-sessions"), undefined, { dir: agentsDir, promptDir: agentPromptDir }, { endpoint: taskEndpoint, todoEndpoint, workflowEndpoint, uploadsDir });
56
+ : new PiRpcRuntime(join(dataDir, "pi-sessions"), undefined, { dir: initialAgentDirs, promptDir: agentPromptDir }, { endpoint: taskEndpoint, todoEndpoint, workflowEndpoint, uploadsDir });
49
57
  const broadcaster = new WsBroadcaster(() => store.snapshot());
50
58
  const extensionUiService = new ExtensionUiService(broadcaster);
51
59
  const supervisor = new SubagentSupervisor(join(dataDir, "pi-sessions"), undefined, {
52
- dir: agentsDir,
60
+ dir: initialAgentDirs,
53
61
  promptDir: agentPromptDir,
54
62
  });
55
63
  const subagents = new SubagentRunRecorder(supervisor, {
@@ -80,10 +88,19 @@ const workflows = new WorkflowEngine(subagents, {
80
88
  },
81
89
  });
82
90
  // ------------------------------------------------------------- Application services
83
- const catalogService = new CatalogService(runtime, broadcaster, agentsDir);
84
- const runService = new RunService(store, runtime, subagents, broadcaster, extensionUiService, agentsDir, workflows);
85
- const conversationService = new ConversationService(store, runtime, subagents, broadcaster, catalogService, runService, extensionUiService, agentsDir);
86
- const workspaceService = new WorkspaceService(store, runtime, broadcaster, catalogService, runService, extensionUiService, uploadsDir, agentsDir);
91
+ const onDirsChanged = (dirs) => {
92
+ paths.agentDirs = dirs;
93
+ paths.agentsDir = dirs[0] ?? "";
94
+ runtime.setAgentsDir?.(dirs);
95
+ subagents.setAgentsDir(dirs);
96
+ runService.setAgentsDir(dirs);
97
+ conversationService.setAgentsDir(dirs);
98
+ workspaceService.setAgentsDir(dirs);
99
+ };
100
+ const catalogService = new CatalogService(runtime, broadcaster, initialAgentDirs, store, rootDir, onDirsChanged);
101
+ const runService = new RunService(store, runtime, subagents, broadcaster, extensionUiService, initialAgentDirs, workflows);
102
+ const conversationService = new ConversationService(store, runtime, subagents, broadcaster, catalogService, runService, extensionUiService, initialAgentDirs);
103
+ const workspaceService = new WorkspaceService(store, runtime, broadcaster, catalogService, runService, extensionUiService, uploadsDir, initialAgentDirs);
87
104
  const serverContext = {
88
105
  paths,
89
106
  store,
@@ -120,7 +137,7 @@ process.on("SIGINT", () => shutdown("SIGINT"));
120
137
  process.on("SIGTERM", () => shutdown("SIGTERM"));
121
138
  // ------------------------------------------------------------- Transports & startup
122
139
  const httpServer = createHttpServer({ context: serverContext, onShutdown: shutdown });
123
- const wsServer = new WsServer({ httpServer, broadcaster, commandHandler, catalogService, accessToken });
140
+ const wsServer = new WsServer({ httpServer, broadcaster, commandHandler, catalogService, accessToken, rootDir });
124
141
  httpServer.listen(port, host, () => {
125
142
  console.log(`Super Pi is running at http://${host}:${port}`);
126
143
  });
@@ -199,6 +199,8 @@ export interface AgentRuntime {
199
199
  * refusal, exactly as `revert`'s absence is, rather than passed over.
200
200
  */
201
201
  fork?(request: ForkRequest): Promise<ForkOutcome>;
202
+ /** Updates the active agent directory or directories to search. */
203
+ setAgentsDir?(dir: string | string[]): void;
202
204
  }
203
205
  /** Deterministic offline runtime used by tests and local UI development. */
204
206
  export declare class WelcomeRuntime implements AgentRuntime {
@@ -527,7 +529,7 @@ export declare class PiRpcRuntime implements AgentRuntime {
527
529
  * call site keeps its meaning - moving the agent directory into that slot would have silently fed a
528
530
  * launcher's file path in as a directory of agent definitions.
529
531
  */
530
- private readonly agents;
532
+ private agents;
531
533
  /**
532
534
  * How the `task` extension reaches this server. A FOURTH positional parameter for the same reason as
533
535
  * the third: an options bag in one of the earlier slots would silently change what an existing
@@ -587,7 +589,7 @@ export declare class PiRpcRuntime implements AgentRuntime {
587
589
  * launcher's file path in as a directory of agent definitions.
588
590
  */
589
591
  agents?: {
590
- dir: string;
592
+ dir: string | string[];
591
593
  promptDir: string;
592
594
  },
593
595
  /**
@@ -624,6 +626,7 @@ export declare class PiRpcRuntime implements AgentRuntime {
624
626
  * as the conversation's own role.
625
627
  */
626
628
  agentFor(conversation: Pick<Conversation, "id" | "agentId">): Promise<AgentDefinition | undefined>;
629
+ setAgentsDir(dir: string | string[]): void;
627
630
  /**
628
631
  * The `--extension` argv for this turn, or `[]` when the task tool is not available.
629
632
  *
@@ -631,9 +631,13 @@ export class PiRpcRuntime {
631
631
  const agent = agents.find((item) => item.id === conversation.agentId);
632
632
  if (agent)
633
633
  return agent;
634
- console.warn(`PiRpcRuntime: conversation ${conversation.id} names agent ${JSON.stringify(conversation.agentId)}, which is not a primary agent in ${this.agents.dir}${problems.length ? ` (${problems.length} file(s) there are not usable agents)` : ""} - running as Pi itself.`);
634
+ const dirLabel = Array.isArray(this.agents.dir) ? this.agents.dir.join(", ") : this.agents.dir;
635
+ console.warn(`PiRpcRuntime: conversation ${conversation.id} names agent ${JSON.stringify(conversation.agentId)}, which is not a primary agent in ${dirLabel}${problems.length ? ` (${problems.length} file(s) there are not usable agents)` : ""} - running as Pi itself.`);
635
636
  return undefined;
636
637
  }
638
+ setAgentsDir(dir) {
639
+ this.agents.dir = dir;
640
+ }
637
641
  /**
638
642
  * The `--extension` argv for this turn, or `[]` when the task tool is not available.
639
643
  *
@@ -677,7 +681,7 @@ export class PiRpcRuntime {
677
681
  async taskExtensionEnv(conversationId) {
678
682
  const { agents, problems } = await readAgents(this.agents.dir, { mode: "subagent" });
679
683
  for (const problem of problems)
680
- console.warn(`PiRpcRuntime: ${this.agents.dir}/${problem.file} is not a usable sub-agent - ${problem.message}`);
684
+ console.warn(`PiRpcRuntime: ${problem.file} is not a usable sub-agent - ${problem.message}`);
681
685
  return {
682
686
  [TASK_ENDPOINT_ENV]: this.options.endpoint ?? "",
683
687
  [TASK_ROSTER_ENV]: rosterPayload(agents),
@@ -14,6 +14,7 @@ export interface ServerPaths {
14
14
  dataDir: string;
15
15
  rootDir: string;
16
16
  agentsDir: string;
17
+ agentDirs?: string[];
17
18
  agentPromptDir?: string;
18
19
  uploadsDir: string;
19
20
  clientDir: string;
@@ -448,6 +448,12 @@ export declare class Store {
448
448
  todos: TodoItem[];
449
449
  dropped: number;
450
450
  }>;
451
+ /**
452
+ * Updates user-configured custom agent discovery directories.
453
+ * Empty/whitespace paths are ignored, and if no valid paths remain, the key is deleted
454
+ * to keep state.json clean.
455
+ */
456
+ setCustomAgentDirs(dirs: string[]): Promise<string[]>;
451
457
  /**
452
458
  * Writes the whole state atomically: a temp file, then a rename over the real one.
453
459
  *
@@ -1194,6 +1194,22 @@ export class Store {
1194
1194
  await this.persist();
1195
1195
  return { todos: structuredClone(todos), dropped };
1196
1196
  }
1197
+ /**
1198
+ * Updates user-configured custom agent discovery directories.
1199
+ * Empty/whitespace paths are ignored, and if no valid paths remain, the key is deleted
1200
+ * to keep state.json clean.
1201
+ */
1202
+ async setCustomAgentDirs(dirs) {
1203
+ const cleaned = Array.from(new Set(dirs.map((d) => d.trim()).filter(Boolean)));
1204
+ if (cleaned.length) {
1205
+ this.state.customAgentDirs = cleaned;
1206
+ }
1207
+ else {
1208
+ delete this.state.customAgentDirs;
1209
+ }
1210
+ await this.persist();
1211
+ return structuredClone(cleaned);
1212
+ }
1197
1213
  /**
1198
1214
  * Writes the whole state atomically: a temp file, then a rename over the real one.
1199
1215
  *
@@ -89,6 +89,8 @@ export declare class SubagentRunRecorder {
89
89
  */
90
90
  private readonly controllers;
91
91
  constructor(supervisor: SubagentSupervisor, hooks: SubagentRunHooks);
92
+ /** Updates agent discovery directories on the underlying supervisor. */
93
+ setAgentsDir(dir: string | string[]): void;
92
94
  /** Every child still running, optionally narrowed to one conversation. The supervisor owns the pids. */
93
95
  liveRuns(conversationId?: string): {
94
96
  id: string;
@@ -44,6 +44,10 @@ export class SubagentRunRecorder {
44
44
  this.supervisor = supervisor;
45
45
  this.hooks = hooks;
46
46
  }
47
+ /** Updates agent discovery directories on the underlying supervisor. */
48
+ setAgentsDir(dir) {
49
+ this.supervisor.setAgentsDir?.(dir);
50
+ }
47
51
  /** Every child still running, optionally narrowed to one conversation. The supervisor owns the pids. */
48
52
  liveRuns(conversationId) {
49
53
  return this.supervisor.liveRuns(conversationId);
@@ -136,7 +136,7 @@ export declare class SubagentCancelled extends Error {
136
136
  /** Spawns sub-agent children Pi and drains their JSONL RPC streams. */
137
137
  export declare class SubagentSupervisor {
138
138
  private readonly sessionDir;
139
- private readonly agents;
139
+ private agents;
140
140
  private readonly pi;
141
141
  /** Live children by run id, so a caller can name the process it means. Cleared in the run's `finally`. */
142
142
  private readonly children;
@@ -147,7 +147,7 @@ export declare class SubagentSupervisor {
147
147
  * child's spawn line depend on when it was started.
148
148
  */
149
149
  constructor(sessionDir: string, command?: string, agents?: {
150
- dir: string;
150
+ dir: string | string[];
151
151
  promptDir: string;
152
152
  });
153
153
  /**
@@ -163,6 +163,7 @@ export declare class SubagentSupervisor {
163
163
  conversationId: string;
164
164
  pid: number | undefined;
165
165
  }[];
166
+ setAgentsDir(dir: string | string[]): void;
166
167
  /**
167
168
  * Runs one sub-agent to completion and returns what it said.
168
169
  *
@@ -195,6 +195,9 @@ export class SubagentSupervisor {
195
195
  liveRuns(conversationId) {
196
196
  return [...this.children].map(([id, run]) => ({ id, conversationId: run.conversationId, pid: run.child.pid }));
197
197
  }
198
+ setAgentsDir(dir) {
199
+ this.agents.dir = dir;
200
+ }
198
201
  /**
199
202
  * Runs one sub-agent to completion and returns what it said.
200
203
  *
@@ -537,6 +540,7 @@ export class SubagentSupervisor {
537
540
  const agent = agents.find((item) => item.id === agentId);
538
541
  if (agent)
539
542
  return agent;
540
- throw new Error(`sub-agent ${JSON.stringify(agentId)} is not a subagent definition in ${this.agents.dir}${problems.length ? ` (${problems.length} file(s) there are not usable agents)` : ""}. A sub-agent is an agents/*.md file with \`mode: subagent\`; a primary agent is chosen by a human and is deliberately not dispatchable as one.`);
543
+ const dirLabel = Array.isArray(this.agents.dir) ? this.agents.dir.join(", ") : this.agents.dir;
544
+ throw new Error(`sub-agent ${JSON.stringify(agentId)} is not a subagent definition in ${dirLabel}${problems.length ? ` (${problems.length} file(s) there are not usable agents)` : ""}. A sub-agent is an agents/*.md file with \`mode: subagent\`; a primary agent is chosen by a human and is deliberately not dispatchable as one.`);
541
545
  }
542
546
  }
@@ -254,6 +254,8 @@ export type AgentDefinition = {
254
254
  */
255
255
  tools?: Record<string, unknown>;
256
256
  prompt: string;
257
+ /** The directory this agent definition was discovered from. */
258
+ sourceDir?: string;
257
259
  };
258
260
  /**
259
261
  * What the picker receives. The prompt body is deliberately NOT sent: it is a whole system prompt,
@@ -261,6 +263,7 @@ export type AgentDefinition = {
261
263
  */
262
264
  export type AgentSummary = Omit<AgentDefinition, "prompt" | "toolFlags" | "tools"> & {
263
265
  toolSummary: string;
266
+ sourceDir?: string;
264
267
  };
265
268
  export type AgentCatalog = {
266
269
  source: "directory" | "unavailable";
@@ -271,6 +274,8 @@ export type AgentCatalog = {
271
274
  message: string;
272
275
  }[];
273
276
  directory: string;
277
+ /** All searched agent directories in priority order. */
278
+ directories?: string[];
274
279
  fetchedAt: string;
275
280
  error?: string;
276
281
  };
@@ -496,6 +501,7 @@ export type AppState = {
496
501
  backgroundTasks: BackgroundTaskSummary[];
497
502
  todos?: TodoItem[];
498
503
  workflows?: WorkflowRun[];
504
+ customAgentDirs?: string[];
499
505
  };
500
506
  export type TodoItem = {
501
507
  id?: string;
@@ -811,12 +817,25 @@ export type ClientCommand = {
811
817
  /** Asks for the `mode: subagent` half of the agent directory. Answered by `subagent.catalog`. */
812
818
  | {
813
819
  type: "subagent.catalog.fetch";
820
+ } | {
821
+ type: "agents.setCustomDirs";
822
+ dirs: string[];
823
+ } | {
824
+ type: "agents.rescan";
814
825
  } | ({
815
826
  type: "extension.ui.answer";
816
827
  conversationId: string;
817
828
  requestId: string;
818
829
  } & ExtensionUiAnswer);
830
+ export type ServerVersionInfo = {
831
+ runningVersion: string;
832
+ diskVersion: string;
833
+ needsRestart: boolean;
834
+ };
819
835
  export type ServerEvent = {
836
+ type: "server.version";
837
+ version: ServerVersionInfo;
838
+ } | {
820
839
  type: "state";
821
840
  state: AppState;
822
841
  } | {
@@ -0,0 +1,10 @@
1
+ import type { ServerVersionInfo } from "./types.js";
2
+ /**
3
+ * Initializes the version captured when this server process booted up.
4
+ */
5
+ export declare function initRunningVersion(rootDir: string): string;
6
+ /**
7
+ * Checks whether the package.json version on disk has been updated beyond
8
+ * the version currently executing in this process.
9
+ */
10
+ export declare function checkDiskVersion(rootDir: string): ServerVersionInfo;
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ let runningVersion = "0.1.3";
4
+ /**
5
+ * Initializes the version captured when this server process booted up.
6
+ */
7
+ export function initRunningVersion(rootDir) {
8
+ try {
9
+ const pkg = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8"));
10
+ if (pkg.version)
11
+ runningVersion = pkg.version;
12
+ }
13
+ catch {
14
+ // fallback to default
15
+ }
16
+ return runningVersion;
17
+ }
18
+ /**
19
+ * Checks whether the package.json version on disk has been updated beyond
20
+ * the version currently executing in this process.
21
+ */
22
+ export function checkDiskVersion(rootDir) {
23
+ try {
24
+ const pkg = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8"));
25
+ const diskVersion = pkg.version ?? runningVersion;
26
+ return {
27
+ runningVersion,
28
+ diskVersion,
29
+ needsRestart: diskVersion !== runningVersion,
30
+ };
31
+ }
32
+ catch {
33
+ return {
34
+ runningVersion,
35
+ diskVersion: runningVersion,
36
+ needsRestart: false,
37
+ };
38
+ }
39
+ }