pikiloom 0.4.26 → 0.4.27

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 (155) hide show
  1. package/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DIP-y4dl.js} +1 -1
  2. package/dashboard/dist/assets/ConnectionModal-BCfvhkiI.js +1 -0
  3. package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-Dot7ua9-.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-GioMx4S6.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C9_r8jJP.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-M1AP1sgR.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-CCm3UUfJ.js} +1 -1
  8. package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-YubDM16P.js} +1 -1
  9. package/dashboard/dist/assets/{SessionPanel-_0dLWvEb.js → SessionPanel-Cnv9o96n.js} +1 -1
  10. package/dashboard/dist/assets/SystemTab-VgNivIe3.js +1 -0
  11. package/dashboard/dist/assets/index-BY7l20bi.js +23 -0
  12. package/dashboard/dist/assets/index-BlqvzDUL.js +3 -0
  13. package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
  14. package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C0SAInnT.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/acp-client.js +0 -59
  17. package/dist/agent/artifacts.js +1 -38
  18. package/dist/agent/auto-update.js +2 -78
  19. package/dist/agent/await-resume.js +0 -20
  20. package/dist/agent/cli/auth.js +3 -51
  21. package/dist/agent/cli/catalog.js +0 -3
  22. package/dist/agent/cli/detector.js +1 -19
  23. package/dist/agent/cli/index.js +0 -3
  24. package/dist/agent/cli/registry.js +0 -25
  25. package/dist/agent/driver.js +0 -14
  26. package/dist/agent/drivers/claude-tui.js +2 -769
  27. package/dist/agent/drivers/claude.js +12 -631
  28. package/dist/agent/drivers/codex.js +11 -162
  29. package/dist/agent/drivers/gemini.js +6 -114
  30. package/dist/agent/drivers/hermes.js +0 -128
  31. package/dist/agent/goal.js +0 -45
  32. package/dist/agent/handover.js +1 -46
  33. package/dist/agent/images.js +7 -133
  34. package/dist/agent/index.js +1 -29
  35. package/dist/agent/mcp/bridge.js +1 -197
  36. package/dist/agent/mcp/extensions.js +5 -84
  37. package/dist/agent/mcp/oauth.js +2 -69
  38. package/dist/agent/mcp/registry.js +2 -33
  39. package/dist/agent/mcp/session-server.js +2 -45
  40. package/dist/agent/mcp/tools/ask-user.js +0 -6
  41. package/dist/agent/mcp/tools/await-resume.js +0 -15
  42. package/dist/agent/mcp/tools/goal.js +0 -25
  43. package/dist/agent/mcp/tools/types.js +0 -5
  44. package/dist/agent/mcp/tools/workspace.js +0 -23
  45. package/dist/agent/npm.js +0 -14
  46. package/dist/agent/session.js +86 -196
  47. package/dist/agent/skill-installer.js +3 -70
  48. package/dist/agent/skills.js +0 -43
  49. package/dist/agent/stream.js +5 -148
  50. package/dist/agent/types.js +0 -12
  51. package/dist/agent/utils.js +2 -120
  52. package/dist/bot/bot.js +3 -468
  53. package/dist/bot/command-ui.js +0 -45
  54. package/dist/bot/commands.js +0 -51
  55. package/dist/bot/headless-bot.js +0 -21
  56. package/dist/bot/host.js +1 -5
  57. package/dist/bot/human-loop.js +0 -8
  58. package/dist/bot/menu.js +0 -3
  59. package/dist/bot/orchestration.js +0 -3
  60. package/dist/bot/render-shared.js +0 -63
  61. package/dist/bot/session-hub.js +14 -92
  62. package/dist/bot/session-status.js +1 -5
  63. package/dist/bot/streaming.js +0 -53
  64. package/dist/browser-profile.js +0 -51
  65. package/dist/browser-supervisor.js +0 -75
  66. package/dist/catalog/cli-tools.js +0 -53
  67. package/dist/catalog/index.js +0 -18
  68. package/dist/catalog/local-models.js +0 -18
  69. package/dist/catalog/mcp-servers.js +0 -35
  70. package/dist/catalog/skill-repos.js +0 -32
  71. package/dist/channels/base.js +0 -19
  72. package/dist/channels/dingtalk/bot.js +0 -3
  73. package/dist/channels/dingtalk/channel.js +0 -33
  74. package/dist/channels/discord/bot.js +0 -7
  75. package/dist/channels/discord/channel.js +0 -22
  76. package/dist/channels/feishu/bot.js +0 -71
  77. package/dist/channels/feishu/channel.js +0 -78
  78. package/dist/channels/feishu/markdown.js +0 -5
  79. package/dist/channels/feishu/render.js +0 -45
  80. package/dist/channels/health.js +0 -46
  81. package/dist/channels/slack/bot.js +0 -7
  82. package/dist/channels/slack/channel.js +1 -27
  83. package/dist/channels/states.js +0 -3
  84. package/dist/channels/telegram/bot.js +0 -60
  85. package/dist/channels/telegram/channel.js +2 -99
  86. package/dist/channels/telegram/directory.js +0 -3
  87. package/dist/channels/telegram/live-preview.js +0 -8
  88. package/dist/channels/telegram/render.js +0 -30
  89. package/dist/channels/wecom/bot.js +0 -3
  90. package/dist/channels/wecom/channel.js +0 -58
  91. package/dist/channels/weixin/api.js +0 -9
  92. package/dist/channels/weixin/bot.js +0 -88
  93. package/dist/channels/weixin/channel.js +0 -6
  94. package/dist/cli/autostart.js +1 -44
  95. package/dist/cli/channel-supervisor.js +1 -59
  96. package/dist/cli/channels.js +0 -6
  97. package/dist/cli/main.js +1 -145
  98. package/dist/cli/onboarding.js +0 -6
  99. package/dist/cli/run.js +0 -11
  100. package/dist/cli/setup-wizard.js +0 -3
  101. package/dist/core/config/runtime-config.js +0 -38
  102. package/dist/core/config/user-config.js +0 -63
  103. package/dist/core/config/validation.js +0 -9
  104. package/dist/core/constants.js +0 -268
  105. package/dist/core/git.js +0 -31
  106. package/dist/core/legacy-compat.js +1 -28
  107. package/dist/core/logging.js +0 -3
  108. package/dist/core/platform.js +0 -28
  109. package/dist/core/process-control.js +0 -8
  110. package/dist/core/secrets/index.js +0 -17
  111. package/dist/core/secrets/inline-seal.js +0 -7
  112. package/dist/core/secrets/ref.js +0 -19
  113. package/dist/core/secrets/resolver.js +0 -8
  114. package/dist/core/secrets/store.js +1 -12
  115. package/dist/core/utils.js +1 -37
  116. package/dist/core/version.js +0 -3
  117. package/dist/dashboard/platform.js +1 -35
  118. package/dist/dashboard/routes/agents.js +2 -71
  119. package/dist/dashboard/routes/cli.js +0 -27
  120. package/dist/dashboard/routes/config.js +0 -54
  121. package/dist/dashboard/routes/extensions.js +0 -132
  122. package/dist/dashboard/routes/local-models.js +0 -68
  123. package/dist/dashboard/routes/models.js +0 -46
  124. package/dist/dashboard/routes/sessions.js +0 -100
  125. package/dist/dashboard/runtime.js +1 -58
  126. package/dist/dashboard/server.js +0 -49
  127. package/dist/dashboard/session-control.js +2 -69
  128. package/dist/model/catalog.js +1 -17
  129. package/dist/model/index.js +0 -15
  130. package/dist/model/injector.js +1 -181
  131. package/dist/model/provider-models.js +1 -48
  132. package/dist/model/responses-bridge.js +3 -45
  133. package/dist/model/store.js +0 -17
  134. package/dist/model/types.js +0 -12
  135. package/dist/model/validation.js +1 -18
  136. package/dist/pikichannel/adapter-pikiloom.js +0 -31
  137. package/dist/pikichannel/code.js +0 -12
  138. package/dist/pikichannel/codec.js +0 -14
  139. package/dist/pikichannel/host.js +4 -33
  140. package/dist/pikichannel/protocol.js +0 -52
  141. package/dist/pikichannel/rendezvous-broker.js +5 -26
  142. package/dist/pikichannel/rendezvous-host.js +3 -16
  143. package/dist/pikichannel/server.js +6 -55
  144. package/dist/pikichannel/transport.js +0 -25
  145. package/dist/pikichannel/transports/webrtc-host.js +0 -18
  146. package/dist/pikichannel/transports/webrtc-shared.js +4 -34
  147. package/dist/pikichannel/transports/websocket-host.js +1 -14
  148. package/dist/pikichannel/turn.js +2 -69
  149. package/dist/pikichannel/web/sdk.js +14 -71
  150. package/package.json +1 -1
  151. package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
  152. package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
  153. package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
  154. package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
  155. package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
@@ -1,38 +1,11 @@
1
- /**
2
- * Skill installer — wrapper around `npx skills` CLI.
3
- *
4
- * Skills are installed via the community-standard `npx skills add` command.
5
- * Global skills go to ~/.pikiloom/skills/, project skills to <workdir>/.pikiloom/skills/.
6
- *
7
- * The upstream CLI doesn't recognize `pikiloom` as an agent, so we install with
8
- * `--agent claude-code` (the driver pikiloom runs by default) and rely on
9
- * ~/.claude/skills → ~/.pikiloom/skills being symlinked to the same directory.
10
- */
11
1
  import { execFile } from 'node:child_process';
12
2
  import fs from 'node:fs';
13
3
  import os from 'node:os';
14
4
  import path from 'node:path';
15
5
  import { STATE_DIR_NAME } from '../core/constants.js';
16
- // ---------------------------------------------------------------------------
17
- // Constants
18
- // ---------------------------------------------------------------------------
19
6
  const GLOBAL_SKILLS_DIR = path.join(os.homedir(), STATE_DIR_NAME, 'skills');
20
7
  const INSTALL_TIMEOUT_MS = 60_000;
21
8
  const REMOVE_TIMEOUT_MS = 10_000;
22
- // ---------------------------------------------------------------------------
23
- // Install ledger — provenance for update detection
24
- //
25
- // `npx skills add` drops skill directories but records no source or version, so
26
- // there's nothing to diff a local install against a moving remote. We keep a
27
- // sidecar ledger next to the skills (one per scope: global beside
28
- // ~/.pikiloom/skills, project beside <workdir>/.pikiloom/skills) mapping each
29
- // installed collection (its GitHub source) to the default-branch commit SHA it
30
- // was pulled from. The dashboard reads the live remote HEAD, diffs it against
31
- // the ledger to surface "update available", then re-runs install to advance it.
32
- //
33
- // The file is a hidden sibling of the skill dirs; skill discovery and removal
34
- // only ever touch named sub-directories, so it's invisible to both.
35
- // ---------------------------------------------------------------------------
36
9
  const SKILL_LEDGER_FILE = '.pikiloom-skills-ledger.json';
37
10
  function ledgerPath(opts) {
38
11
  if (opts.global)
@@ -41,7 +14,6 @@ function ledgerPath(opts) {
41
14
  return path.join(opts.workdir, STATE_DIR_NAME, 'skills', SKILL_LEDGER_FILE);
42
15
  return null;
43
16
  }
44
- /** Normalize a source to a stable key: lowercase `owner/repo`, URL/.git stripped. */
45
17
  export function normalizeSkillSourceKey(source) {
46
18
  return String(source || '')
47
19
  .trim()
@@ -60,7 +32,7 @@ function readLedger(opts) {
60
32
  return { version: 1, entries: parsed.entries };
61
33
  }
62
34
  }
63
- catch { /* missing or corrupt — treat as empty */ }
35
+ catch { }
64
36
  return { version: 1, entries: {} };
65
37
  }
66
38
  function writeLedger(ledger, opts) {
@@ -71,16 +43,14 @@ function writeLedger(ledger, opts) {
71
43
  fs.mkdirSync(path.dirname(p), { recursive: true });
72
44
  fs.writeFileSync(p, JSON.stringify(ledger, null, 2));
73
45
  }
74
- catch { /* best effort — provenance is non-critical */ }
46
+ catch { }
75
47
  }
76
- /** Look up the recorded provenance for a source in the given scope. */
77
48
  export function getSkillLedgerEntry(source, opts) {
78
49
  const key = normalizeSkillSourceKey(source);
79
50
  if (!key)
80
51
  return null;
81
52
  return readLedger(opts).entries[key] || null;
82
53
  }
83
- /** Record (or refresh) the provenance for a source after a successful install/update. */
84
54
  export function recordSkillInstall(source, opts) {
85
55
  const key = normalizeSkillSourceKey(source);
86
56
  if (!key)
@@ -95,7 +65,6 @@ export function recordSkillInstall(source, opts) {
95
65
  };
96
66
  writeLedger(ledger, opts);
97
67
  }
98
- /** Drop a source's provenance — call when its skills are removed. */
99
68
  export function forgetSkillInstall(source, opts) {
100
69
  const key = normalizeSkillSourceKey(source);
101
70
  if (!key)
@@ -106,15 +75,6 @@ export function forgetSkillInstall(source, opts) {
106
75
  writeLedger(ledger, opts);
107
76
  }
108
77
  }
109
- // ---------------------------------------------------------------------------
110
- // Helpers
111
- // ---------------------------------------------------------------------------
112
- /**
113
- * Make sure the global skills directory exists, and that the agent-specific
114
- * dirs that the upstream skills CLI writes to (`~/.claude/skills`,
115
- * `~/.agents/skills`) resolve back to it. This is what lets us install with
116
- * `--agent claude-code` and still read the results from `~/.pikiloom/skills`.
117
- */
118
78
  function ensureGlobalSkillsDir() {
119
79
  fs.mkdirSync(GLOBAL_SKILLS_DIR, { recursive: true });
120
80
  for (const linkDir of [
@@ -128,7 +88,6 @@ function ensureGlobalSkillsDir() {
128
88
  if (real === fs.realpathSync(GLOBAL_SKILLS_DIR))
129
89
  continue;
130
90
  }
131
- // Existing dir/link doesn't match — leave it alone rather than destroy user data.
132
91
  continue;
133
92
  }
134
93
  catch {
@@ -136,7 +95,7 @@ function ensureGlobalSkillsDir() {
136
95
  fs.mkdirSync(path.dirname(linkDir), { recursive: true });
137
96
  fs.symlinkSync(GLOBAL_SKILLS_DIR, linkDir, 'dir');
138
97
  }
139
- catch { /* best effort */ }
98
+ catch { }
140
99
  }
141
100
  }
142
101
  }
@@ -154,18 +113,9 @@ function runNpx(args, cwd, timeoutMs) {
154
113
  stderr: stderr?.toString() || '',
155
114
  });
156
115
  });
157
- // Prevent child from keeping parent alive
158
116
  child.unref?.();
159
117
  });
160
118
  }
161
- // ---------------------------------------------------------------------------
162
- // Install
163
- // ---------------------------------------------------------------------------
164
- /**
165
- * Install a skill from a source (GitHub owner/repo, URL, or local path).
166
- *
167
- * Uses `npx skills add <source>` with appropriate flags.
168
- */
169
119
  export async function installSkill(source, opts = {}) {
170
120
  const { global: isGlobal, skill, workdir } = opts;
171
121
  if (!isGlobal && !workdir) {
@@ -185,8 +135,6 @@ export async function installSkill(source, opts = {}) {
185
135
  const errorMsg = result.stderr.trim().split('\n').pop()?.trim() || 'installation failed';
186
136
  return { ok: false, error: errorMsg, output: result.stdout + result.stderr };
187
137
  }
188
- // Record provenance so the dashboard can later detect remote updates. Scoped
189
- // to where the skill landed; best-effort, never fails the install.
190
138
  recordSkillInstall(source, {
191
139
  global: isGlobal,
192
140
  workdir,
@@ -195,19 +143,11 @@ export async function installSkill(source, opts = {}) {
195
143
  });
196
144
  return { ok: true, output: result.stdout };
197
145
  }
198
- // ---------------------------------------------------------------------------
199
- // Remove
200
- // ---------------------------------------------------------------------------
201
- /**
202
- * Remove an installed skill by name.
203
- * Deletes the skill directory from the appropriate location.
204
- */
205
146
  export function removeSkill(skillName, opts = {}) {
206
147
  const { global: isGlobal, workdir } = opts;
207
148
  if (!isGlobal && !workdir) {
208
149
  return { ok: false, error: 'workdir is required for project-scoped skill removal' };
209
150
  }
210
- // Security: prevent path traversal — skill name must be a plain directory name
211
151
  const sanitized = path.basename(skillName);
212
152
  if (!sanitized || sanitized === '.' || sanitized === '..' || sanitized !== skillName) {
213
153
  return { ok: false, error: 'invalid skill name' };
@@ -216,7 +156,6 @@ export function removeSkill(skillName, opts = {}) {
216
156
  ? GLOBAL_SKILLS_DIR
217
157
  : path.join(workdir, STATE_DIR_NAME, 'skills');
218
158
  const skillDir = path.join(parentDir, sanitized);
219
- // Double-check the resolved path is inside the expected parent
220
159
  const realParent = path.resolve(parentDir);
221
160
  const realSkill = path.resolve(skillDir);
222
161
  if (!realSkill.startsWith(realParent + path.sep)) {
@@ -233,15 +172,9 @@ export function removeSkill(skillName, opts = {}) {
233
172
  return { ok: false, error: e?.message || 'removal failed' };
234
173
  }
235
174
  }
236
- // ---------------------------------------------------------------------------
237
- // List installed (enhanced)
238
- // ---------------------------------------------------------------------------
239
175
  export function getGlobalSkillsDir() {
240
176
  return GLOBAL_SKILLS_DIR;
241
177
  }
242
- // ---------------------------------------------------------------------------
243
- // Check for updates
244
- // ---------------------------------------------------------------------------
245
178
  export async function checkSkillUpdates(opts = {}) {
246
179
  const cwd = opts.global ? os.homedir() : (opts.workdir || process.cwd());
247
180
  const args = ['-y', 'skills', 'check'];
@@ -1,6 +1,3 @@
1
- /**
2
- * Project skill discovery from .pikiloom/skills and .claude/commands.
3
- */
4
1
  import fs from 'node:fs';
5
2
  import os from 'node:os';
6
3
  import path from 'node:path';
@@ -28,7 +25,6 @@ function parseSkillMeta(content) {
28
25
  const dm = fm[1].match(/^description:\s*(.+)/m);
29
26
  if (dm)
30
27
  description = dm[1].trim();
31
- // Parse mcp_requires as YAML list
32
28
  const mr = fm[1].match(/^mcp_requires:\s*\n((?:\s+-\s+.+\n?)+)/m);
33
29
  if (mr) {
34
30
  mcpRequires = mr[1]
@@ -105,11 +101,6 @@ function ensureDirSymlink(linkPath, targetDir) {
105
101
  const desiredReal = realPathOrNull(targetDir);
106
102
  if (currentTarget === desiredTarget || (currentReal && desiredReal && currentReal === desiredReal))
107
103
  return;
108
- // Symlink points elsewhere — including dangling legacy targets like the
109
- // pre-rename `../.pikiclaw/skills`. `rmSync(recursive,force)` silently
110
- // no-ops on a dangling symlink (it follows the link, sees the target is
111
- // gone, treats it as already deleted) and leaves the link in place, which
112
- // then trips EEXIST below. `unlinkSync` removes the link itself reliably.
113
104
  fs.unlinkSync(linkPath);
114
105
  }
115
106
  else {
@@ -122,9 +113,6 @@ function ensureDirSymlink(linkPath, targetDir) {
122
113
  fs.symlinkSync(desiredTarget, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
123
114
  }
124
115
  catch (err) {
125
- // Tolerate a concurrent creator (the prod self-bootstrap and `npm run dev`
126
- // can both initialize the same workdir) — if the link now resolves to the
127
- // intended target, the race is benign; otherwise surface the real error.
128
116
  if (err?.code !== 'EEXIST' || fs.readlinkSync(linkPath) !== desiredTarget)
129
117
  throw err;
130
118
  }
@@ -169,33 +157,13 @@ export function getProjectSkillPaths(workdir, skillName) {
169
157
  claudeSkillFile: hasFile(claudeSkillFile) ? claudeSkillFile : null,
170
158
  };
171
159
  }
172
- // Matches the canonical prompt produced by `resolveSkillPrompt` (bot/commands)
173
- // and `resolveSkillFromPrompt` (dashboard/session-control). Both build the same
174
- // shape: `[Project directory: <wd>]\n\nRead the skill definition at \`<path>\`
175
- // and execute the instructions defined there.[ Additional context: <args>]`.
176
- //
177
- // Whitespace between the segments is tolerant (`\s+`) so the regex still
178
- // matches after the claude driver collapses interior `\s+` to single spaces
179
- // when surfacing user text in `getClaudeSessionMessages`.
180
160
  const SKILL_PROMPT_RE = /^\[Project directory: [^\]\n]+?\]\s+Read the skill definition at `([^`\n]+)` and execute the instructions defined there\.(?:\s+Additional context:\s+([\s\S]+?))?\s*$/;
181
- /**
182
- * Inverse of `resolveSkillPrompt`. When a stored user message matches the
183
- * canonical skill-execution expansion, return the original `/skillname [args]`
184
- * shorthand for display. Returns null when the text isn't a recognized skill
185
- * prompt — callers should fall back to the raw text.
186
- *
187
- * The expanded form is what the agent CLI actually consumed and what gets
188
- * persisted to its session log; this collapse exists purely so the dashboard
189
- * (and other display surfaces) can render the slash command the user typed.
190
- */
191
161
  export function collapseSkillPrompt(text) {
192
162
  if (!text)
193
163
  return null;
194
164
  const m = SKILL_PROMPT_RE.exec(text);
195
165
  if (!m)
196
166
  return null;
197
- // Skill files are always at `<root>/<skillName>/SKILL.md`. Split on both
198
- // `/` and `\` so Windows-generated paths (path.join) resolve correctly.
199
167
  const segments = m[1].split(/[/\\]/).filter(Boolean);
200
168
  if (segments.length < 2 || segments[segments.length - 1] !== 'SKILL.md')
201
169
  return null;
@@ -206,9 +174,6 @@ export function collapseSkillPrompt(text) {
206
174
  return args ? `/${name} ${args}` : `/${name}`;
207
175
  }
208
176
  const GLOBAL_SKILLS_ROOT = path.join(os.homedir(), STATE_DIR_NAME, 'skills');
209
- // Per-file cache of parsed SKILL.md metadata, keyed by file mtime. listSkills runs
210
- // on every skills-menu render (IM + dashboard); without this it re-read and
211
- // re-regex-parsed every SKILL.md each time. A changed skill re-parses just itself.
212
177
  const skillMetaCache = new Map();
213
178
  function discoverSkillsFromDir(dir, scope, seen) {
214
179
  const skills = [];
@@ -251,24 +216,16 @@ function discoverSkillsFromDir(dir, scope, seen) {
251
216
  }
252
217
  return skills;
253
218
  }
254
- /**
255
- * List all skills — project-scoped (workdir) first, then global (~/.pikiloom/skills/).
256
- * Project skills with the same name shadow global ones.
257
- */
258
219
  export function listSkills(workdir) {
259
220
  const seen = new Set();
260
221
  const { canonicalRoot, legacyRoots } = resolveProjectSkillRoots(workdir);
261
- // Project skills take precedence. Also scan the pre-rename `.pikiclaw/skills`
262
- // dir so repos that committed project skills before the rename keep working.
263
222
  const projectSkills = [
264
223
  ...discoverSkillsFromDir(canonicalRoot, 'project', seen),
265
224
  ...legacyRoots.flatMap(root => discoverSkillsFromDir(root, 'project', seen)),
266
225
  ];
267
- // Global skills fill in the rest
268
226
  const globalSkills = discoverSkillsFromDir(GLOBAL_SKILLS_ROOT, 'global', seen);
269
227
  return { skills: [...projectSkills, ...globalSkills], workdir };
270
228
  }
271
- /** Return the global skills root directory path. */
272
229
  export function getGlobalSkillsRoot() {
273
230
  return GLOBAL_SKILLS_ROOT;
274
231
  }
@@ -1,6 +1,3 @@
1
- /**
2
- * CLI spawn framework, stream orchestration, agent detection, and driver delegation.
3
- */
4
1
  import { execSync, spawn } from 'node:child_process';
5
2
  import { createInterface } from 'node:readline';
6
3
  import fs from 'node:fs';
@@ -12,12 +9,9 @@ import { awaitAgentUpdateIdle } from './auto-update.js';
12
9
  import { getDriver, allDrivers, getAcceptedProviderKinds, hasDriver } from './driver.js';
13
10
  import { resolveAgentInjection, getActiveProfile, getActiveProfileId, getProvider, updateProfile, listProfiles, } from '../model/index.js';
14
11
  import { Q, agentLog, agentWarn, agentError, joinErrorMessages, normalizeErrorMessage, buildStreamPreviewMeta, computeContext, shortValue, isPendingSessionId, dedupeStrings, normalizeStreamPreviewPlan, } from './utils.js';
15
- import { saveSessionRecord, setSessionRunState, applySessionRunResult, ensureSessionWorkspace, importFilesIntoWorkspace, syncManagedSessionIdentity, summarizePromptTitle, recordFork, } from './session.js';
12
+ import { saveSessionRecord, setSessionRunState, applySessionRunResult, ensureSessionWorkspace, importFilesIntoWorkspace, syncManagedSessionIdentity, summarizePromptTitle, recordFork, resolveCanonicalSessionId, } from './session.js';
16
13
  import { clearAwaitResume } from './await-resume.js';
17
14
  import { collapseSkillPrompt } from './skills.js';
18
- // ---------------------------------------------------------------------------
19
- // Private helpers
20
- // ---------------------------------------------------------------------------
21
15
  function trimSessionText(value, max = 24_000) {
22
16
  const text = typeof value === 'string' ? value.trim() : '';
23
17
  if (!text)
@@ -26,14 +20,6 @@ function trimSessionText(value, max = 24_000) {
26
20
  return text;
27
21
  return `${text.slice(0, Math.max(0, max - 3)).trimEnd()}...`;
28
22
  }
29
- /**
30
- * Spot known browser-MCP failure signatures inside an agent stdout line so the
31
- * supervisor can force-restart Chrome before the next turn. Both patterns are
32
- * narrow on purpose: `Frame has been detached` is playwright-specific; the
33
- * `Connection closed` MCP error only triggers when the same line names the
34
- * `pikiloom-browser` server, so failures on other MCP servers do not nuke the
35
- * managed browser. The supervisor itself debounces, so this can fire freely.
36
- */
37
23
  export function _detectBrowserMcpFailure(rawLine) {
38
24
  if (!rawLine)
39
25
  return null;
@@ -44,9 +30,6 @@ export function _detectBrowserMcpFailure(rawLine) {
44
30
  }
45
31
  return null;
46
32
  }
47
- // ---------------------------------------------------------------------------
48
- // Agent detection (private helpers and cache)
49
- // ---------------------------------------------------------------------------
50
33
  const AGENT_DETECT_TTL_MS = AGENT_DETECT_TIMEOUTS.detectTtl;
51
34
  const AGENT_VERSION_TTL_MS = AGENT_DETECT_TIMEOUTS.versionTtl;
52
35
  const AGENT_VERSION_TIMEOUT_MS = AGENT_DETECT_TIMEOUTS.versionCommand;
@@ -114,7 +97,6 @@ function readAgentVersion(binPath, timeoutMs) {
114
97
  return null;
115
98
  }
116
99
  }
117
- // Agent detection (used by all drivers)
118
100
  export function detectAgentBin(cmd, agent, options = {}) {
119
101
  const cacheKey = `${agent}:${cmd}`;
120
102
  const now = Date.now();
@@ -155,23 +137,6 @@ export function detectAgentBin(cmd, agent, options = {}) {
155
137
  export function listAgents(options = {}) {
156
138
  return { agents: allDrivers().map(d => detectAgentBin(d.cmd, d.id, options)) };
157
139
  }
158
- /**
159
- * Resolve the *effective* default agent for new conversations.
160
- *
161
- * The stored value is only a *preference* — a new conversation can run only an
162
- * agent whose CLI is actually installed. So when the preference's CLI isn't
163
- * installed, we clamp to the first installed agent (in driver-registration
164
- * order: claude → codex → gemini → hermes) instead of surfacing an uninstalled
165
- * default the user can't run. When the preference *is* installed it always
166
- * wins, so machines with the historical 'codex' default are unaffected. When
167
- * nothing is installed we keep the prior behaviour (honour a valid preference,
168
- * else 'codex') so the result is always defined.
169
- *
170
- * Resolution is derived, never persisted: if the user later installs their
171
- * preferred agent, the original preference is honoured again automatically.
172
- * `agents` is injected (defaults to live detection) so the resolution is a pure
173
- * function of (preference, install-state) and trivially testable.
174
- */
175
140
  export function resolveDefaultAgent(preferred, agents = listAgents().agents) {
176
141
  const want = typeof preferred === 'string' ? preferred.trim().toLowerCase() : '';
177
142
  const wantValid = !!want && hasDriver(want);
@@ -182,9 +147,6 @@ export function resolveDefaultAgent(preferred, agents = listAgents().agents) {
182
147
  return installed[0];
183
148
  return wantValid ? want : 'codex';
184
149
  }
185
- // ---------------------------------------------------------------------------
186
- // Shared CLI spawn framework (used by driver-claude.ts, driver-gemini.ts)
187
- // ---------------------------------------------------------------------------
188
150
  export async function run(cmd, opts, parseLine, parseStderrLine) {
189
151
  const start = Date.now();
190
152
  const deadline = start + opts.timeout * 1000;
@@ -192,10 +154,6 @@ export async function run(cmd, opts, parseLine, parseStderrLine) {
192
154
  let lineCount = 0;
193
155
  let timedOut = false;
194
156
  let interrupted = false;
195
- // BYOK: seed contextWindow from the provider-cached value so the live
196
- // preview percent uses the real denominator (e.g. 1M for DeepSeek v4 Pro
197
- // via OpenRouter) instead of whatever the CLI happens to report later.
198
- // Parsers gate their cc/codex-advertised updates on `s.byokContextWindow`.
199
157
  const byokWindow = opts.byokContextWindow && opts.byokContextWindow > 0
200
158
  ? opts.byokContextWindow
201
159
  : null;
@@ -205,8 +163,6 @@ export async function run(cmd, opts, parseLine, parseStderrLine) {
205
163
  model: opts.model, thinkingEffort: opts.thinkingEffort, errors: null,
206
164
  inputTokens: null, outputTokens: null, cachedInputTokens: null,
207
165
  cacheCreationInputTokens: null,
208
- // Output tokens from this turn's finished LLM calls — folded in by parsers
209
- // when a new call resets the per-call counter (claude message_start).
210
166
  turnOutputTokensBase: 0,
211
167
  contextWindow: byokWindow,
212
168
  byokContextWindow: byokWindow,
@@ -218,14 +174,8 @@ export async function run(cmd, opts, parseLine, parseStderrLine) {
218
174
  claudeToolsById: new Map(),
219
175
  seenClaudeToolIds: new Set(),
220
176
  geminiToolsById: new Map(),
221
- // Claude-only: sub-agent invocations from the Task tool. Other drivers leave it empty.
222
177
  subAgents: new Map(),
223
- // Image blocks collected during the stream (assistant images, MCP tool
224
- // results, …). Surfaced on the StreamResult so IM channels can dispatch
225
- // them at end-of-turn without re-reading the session file.
226
178
  imageBlocks: [],
227
- // Wired to opts.onSessionId so parsers can broadcast id changes the instant
228
- // the CLI surfaces them (see emitSessionIdUpdate in agent/utils.ts).
229
179
  _emitSessionId: opts.onSessionId ?? null,
230
180
  };
231
181
  const shellCmd = cmd.map(Q).join(' ');
@@ -243,9 +193,6 @@ export async function run(cmd, opts, parseLine, parseStderrLine) {
243
193
  });
244
194
  agentLog(`[spawn] pid=${proc.pid}`);
245
195
  proc.stdin?.on('error', (e) => {
246
- // Some CLIs can exit after producing all output before we finish writing
247
- // stdin. Treat EPIPE as a normal early-close race; the exit code/stdout
248
- // still determine the stream result.
249
196
  if (e?.code !== 'EPIPE')
250
197
  agentWarn(`[stdin] write failed: ${e?.message || e}`);
251
198
  });
@@ -370,38 +317,26 @@ export async function run(cmd, opts, parseLine, parseStderrLine) {
370
317
  assistantBlocks: s.imageBlocks.length ? [...s.imageBlocks] : undefined,
371
318
  };
372
319
  }
373
- // ---------------------------------------------------------------------------
374
- // Stream orchestration
375
- // ---------------------------------------------------------------------------
376
320
  function prepareStreamOpts(opts) {
377
- // For display fields (title / lastQuestion / lastMessageText) prefer the
378
- // `/skillname` shorthand the user typed over the long expansion we
379
- // synthesized for the agent — the expanded form is what the CLI consumes,
380
- // but it shouldn't leak into session list previews or sidebar tabs.
381
321
  const displayPrompt = collapseSkillPrompt(opts.prompt) ?? opts.prompt;
382
- const session = ensureSessionWorkspace({ agent: opts.agent, workdir: opts.workdir, sessionId: opts.sessionId, title: displayPrompt });
322
+ const resolvedInboundSessionId = opts.sessionId
323
+ ? resolveCanonicalSessionId(opts.workdir, opts.agent, opts.sessionId)
324
+ : opts.sessionId;
325
+ const session = ensureSessionWorkspace({ agent: opts.agent, workdir: opts.workdir, sessionId: resolvedInboundSessionId, title: displayPrompt });
383
326
  const importedFiles = importFilesIntoWorkspace(session.workspacePath, opts.attachments || []);
384
327
  const attachmentRelPaths = dedupeStrings([...session.record.stagedFiles, ...importedFiles]);
385
- // Capture staged files for MCP bridge before clearing
386
328
  const stagedFiles = [...session.record.stagedFiles];
387
329
  session.record.stagedFiles = [];
388
- // Remember this turn's attachments so dashboard fallbacks (called while the
389
- // agent CLI hasn't yet flushed the user event to its native session file)
390
- // can still render the user's image bubble. Cleared/overwritten at the
391
- // start of the NEXT turn — always reflects the turn currently in flight.
392
330
  session.record.lastUserAttachments = [...attachmentRelPaths];
393
331
  if (!session.record.title)
394
332
  session.record.title = summarizePromptTitle(displayPrompt) || null;
395
333
  session.record.lastQuestion = shortValue(displayPrompt, 500);
396
334
  session.record.lastMessageText = shortValue(displayPrompt, 500);
397
335
  setSessionRunState(session.record, 'running', null);
398
- // A turn starting clears any "waiting on background work" marker the previous
399
- // turn parked — the session is plainly running again, not waiting.
400
336
  if (session.sessionId)
401
337
  clearAwaitResume(opts.workdir, opts.agent, session.sessionId);
402
338
  saveSessionRecord(opts.workdir, session.record);
403
339
  const attachmentPaths = attachmentRelPaths.map(relPath => path.join(session.workspacePath, relPath));
404
- // For pending sessions, pass null sessionId to the CLI so it creates a new session
405
340
  const effectiveSessionId = isPendingSessionId(session.sessionId) ? null : session.sessionId;
406
341
  return {
407
342
  session,
@@ -431,19 +366,12 @@ function finalizeStreamResult(result, workdir, prompt, session, workflowEnabled)
431
366
  session.record.model = result.model || session.record.model;
432
367
  if (result.thinkingEffort)
433
368
  session.record.thinkingEffort = result.thinkingEffort;
434
- // Remember whether this turn ran with Workflow on so the synthetic `ultra`
435
- // rung re-folds for display after the live stream ends and on resume — the
436
- // stored `thinkingEffort` stays the concrete rung (e.g. `max`). `undefined`
437
- // (driver invoked outside the bot) leaves the prior value untouched.
438
369
  if (workflowEnabled !== undefined)
439
370
  session.record.workflowEnabled = workflowEnabled;
440
- // Capture the BYOK Profile that was in effect for this run so a future
441
- // `session.switch` can re-bind it (null = native CLI auth).
442
371
  try {
443
372
  session.record.profileId = getActiveProfileId(session.record.agent);
444
373
  }
445
374
  catch {
446
- /* model layer not initialised in tests — leave profileId untouched */
447
375
  }
448
376
  const displayPrompt = collapseSkillPrompt(prompt) ?? prompt;
449
377
  if (!session.record.title)
@@ -477,7 +405,6 @@ export async function doStream(opts) {
477
405
  codexCumulative: null, error: message, stopReason: null, incomplete: true, activity: null, plan: null,
478
406
  };
479
407
  }
480
- // Start MCP bridge for IM tools (when sendFile is available) and/or supplemental servers (browser, etc.)
481
408
  let bridge = null;
482
409
  try {
483
410
  const { startMcpBridge, redactMcpConfigForLog } = await import('./mcp/bridge.js');
@@ -515,9 +442,6 @@ export async function doStream(opts) {
515
442
  catch (e) {
516
443
  agentWarn(`[mcp] bridge start failed: ${e.message} — proceeding without MCP`);
517
444
  }
518
- // Apply BYOK injection (Provider/Profile from the model layer): merges env
519
- // vars into prepared.extraEnv, overrides the per-agent model field, and
520
- // hands argvAppend to drivers that consume it (Hermes via opts.extraEnv → its own argv builder).
521
445
  try {
522
446
  const injection = await resolveAgentInjection(prepared.agent);
523
447
  if (injection) {
@@ -548,9 +472,6 @@ export async function doStream(opts) {
548
472
  }
549
473
  agentLog(`[byok] ${injection.detail}`);
550
474
  }
551
- // resolveAgentEffort (runtime-config) reads only the top-level hermesReasoningEffort
552
- // field and cannot see the effort stored inside models.profiles[].effort. Override
553
- // thinkingEffort here so the Profile's effort wins over the config default.
554
475
  const activeProfile = getActiveProfile(prepared.agent);
555
476
  if (activeProfile?.effort) {
556
477
  prepared.thinkingEffort = activeProfile.effort;
@@ -559,13 +480,6 @@ export async function doStream(opts) {
559
480
  catch (e) {
560
481
  agentWarn(`[byok] failed to apply Profile injection: ${e?.message || e}`);
561
482
  }
562
- // In-memory-first: stamp the turn's resolved reasoning rung + Workflow opt-in
563
- // onto the centralized index NOW — before the agent CLI has flushed its own
564
- // session file — so the session list/composer reflect the user's pick during
565
- // the very first turn instead of only after finalizeStreamResult. The managed
566
- // record is the single source of truth for this metadata and links to the
567
- // native agent-session by id on promotion; finalize re-stamps it (plus the
568
- // actual model) authoritatively at turn end.
569
483
  try {
570
484
  if (prepared.thinkingEffort) {
571
485
  session.record.thinkingEffort = prepared.thinkingEffort.trim().toLowerCase() || session.record.thinkingEffort;
@@ -583,18 +497,9 @@ export async function doStream(opts) {
583
497
  if (opts.forkOf && !driver.capabilities?.fork) {
584
498
  throw new Error(`Agent ${prepared.agent} does not support fork`);
585
499
  }
586
- // A background agent-CLI auto-update (`npm install -g` / `brew upgrade`, by
587
- // this process OR the `npx pikiloom@latest` self-bootstrap) briefly removes
588
- // the bin while it relinks; exec'ing into that window fails with exit 127
589
- // "command not found". Wait out any in-flight reinstall of THIS agent before
590
- // dispatching to the driver — this is the one chokepoint every agent turn
591
- // (claude -p, claude TUI, codex app-server, gemini) passes through. No-op
592
- // when nothing is updating.
593
500
  await awaitAgentUpdateIdle(prepared.agent, AGENT_UPDATE_TIMEOUTS.spawnWait);
594
501
  const result = await driver.doStream(prepared);
595
502
  const finalized = finalizeStreamResult(result, opts.workdir, opts.prompt, session, opts.claudeWorkflowEnabled);
596
- // Once the child has its real session ID, link the lineage. We do this
597
- // after finalize so the child record is persisted with its native ID.
598
503
  if (opts.forkOf && finalized.sessionId) {
599
504
  try {
600
505
  recordFork(opts.workdir, {
@@ -651,9 +556,6 @@ export async function doStream(opts) {
651
556
  }
652
557
  }
653
558
  }
654
- // ---------------------------------------------------------------------------
655
- // Driver delegation
656
- // ---------------------------------------------------------------------------
657
559
  export function getSessions(opts) {
658
560
  const workdir = path.resolve(opts.workdir);
659
561
  agentLog(`[sessions] request agent=${opts.agent} workdir=${workdir} limit=${opts.limit ?? 'all'}`);
@@ -671,37 +573,11 @@ export function getSessionMessages(opts) {
671
573
  export function listModels(agent, opts = {}) {
672
574
  return getDriver(agent).listModels(opts);
673
575
  }
674
- /**
675
- * Detect a Provider whose baseURL is on the local machine (Ollama / mlx-lm
676
- * connected via `/api/local-models/connect`). Used only to bucket the entry
677
- * into the `'local'` group in the unified picker — runtime behaviour is
678
- * unchanged whether or not the baseURL is loopback.
679
- */
680
576
  function isLocalProviderBaseURL(baseURL) {
681
577
  return /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::|\/|$)/i.test(baseURL);
682
578
  }
683
- /**
684
- * Resolve the model list a UI surface should show for `agent`.
685
- *
686
- * Returns a *union* of:
687
- * 1. The agent CLI's native model catalogue (no Profile required), tagged
688
- * `group: 'native'`.
689
- * 2. Every Profile whose provider kind appears in the driver's
690
- * `acceptedProviderKinds`, tagged `group: 'cloud'` (remote BYOK) or
691
- * `group: 'local'` (loopback baseURL — Ollama / mlx-lm).
692
- *
693
- * The previous behaviour — filter to the active Profile's provider — meant
694
- * users could not switch *across* providers from the IM picker without first
695
- * unbinding through the dashboard. The unified list removes that step so
696
- * `/models` is a one-screen pick.
697
- *
698
- * Callers that need the strictly-native list (e.g. the dashboard agent card's
699
- * "Native" branch) should call `driver.listModels()` directly — this function
700
- * is for the unified picker.
701
- */
702
579
  export async function resolveAgentModels(agent, opts = {}) {
703
580
  const driver = getDriver(agent);
704
- // 1. Native — agent CLI's built-in catalogue.
705
581
  let nativeResult;
706
582
  try {
707
583
  nativeResult = await driver.listModels(opts);
@@ -714,10 +590,6 @@ export async function resolveAgentModels(agent, opts = {}) {
714
590
  alias: m.alias,
715
591
  group: 'native',
716
592
  }));
717
- // 2. BYOK Profiles compatible with this driver — grouped into cloud vs local
718
- // by baseURL. We never call the provider's /models endpoint here: that
719
- // list can run into the hundreds for OpenRouter and would drown the
720
- // picker. Profiles ARE the curated middle layer.
721
593
  const acceptedKinds = new Set(getAcceptedProviderKinds(agent));
722
594
  const cloud = [];
723
595
  const local = [];
@@ -754,25 +626,10 @@ export async function resolveAgentModels(agent, opts = {}) {
754
626
  export function getUsage(opts) {
755
627
  return getDriver(opts.agent).getUsage(opts);
756
628
  }
757
- /**
758
- * If the user has a BYOK Profile bound to `agent`, return its raw modelId
759
- * (e.g. "deepseek/deepseek-v4-flash"). Returns null when no profile is bound.
760
- * Used by display paths that need to show the profile's model rather than the
761
- * pikiloom user-config model (which may be stale or unrelated to the active profile).
762
- */
763
629
  export function getAgentBoundModelId(agent) {
764
630
  const profile = getActiveProfile(agent);
765
631
  return profile?.modelId ?? null;
766
632
  }
767
- /**
768
- * Persist a model id to the active BYOK Profile for `agent`. Returns true when
769
- * the Profile was updated (caller should skip writing the legacy
770
- * `<agent>Model` user-config field), false when no Profile is bound.
771
- *
772
- * Hermes uses this as the *primary* persistence path because `hermes acp` does
773
- * not support runtime model switching via CLI flags — the only way to change
774
- * the model is the Profile (which the driver passes to ACP `session/set_model`).
775
- */
776
633
  export function setAgentBoundModelId(agent, modelId) {
777
634
  const profile = getActiveProfile(agent);
778
635
  if (!profile)
@@ -1,13 +1 @@
1
- /**
2
- * types.ts — Shared type definitions for the agent layer.
3
- *
4
- * All type aliases, interfaces, and type-level constants used across the
5
- * agent subsystem (drivers, session management, streaming, listing, usage)
6
- * are centralised here so that consumers can import types without pulling
7
- * in runtime code.
8
- */
9
- // ---------------------------------------------------------------------------
10
- // Constants
11
- // ---------------------------------------------------------------------------
12
- /** Set of image file extensions recognised for workspace file handling. */
13
1
  export const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);