@specship/specship-darwin-arm64 0.29.0 → 0.30.0
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.
- package/bin/specship +1 -1
- package/lib/dist/.claude-plugin/plugin.json +15 -3
- package/lib/dist/activation/cheatsheet.d.ts +10 -7
- package/lib/dist/activation/cheatsheet.d.ts.map +1 -1
- package/lib/dist/activation/cheatsheet.js +21 -5
- package/lib/dist/activation/cheatsheet.js.map +1 -1
- package/lib/dist/activation/hook-output.d.ts +75 -2
- package/lib/dist/activation/hook-output.d.ts.map +1 -1
- package/lib/dist/activation/hook-output.js +114 -3
- package/lib/dist/activation/hook-output.js.map +1 -1
- package/lib/dist/activation/search-intercept.d.ts +20 -7
- package/lib/dist/activation/search-intercept.d.ts.map +1 -1
- package/lib/dist/activation/search-intercept.js +27 -5
- package/lib/dist/activation/search-intercept.js.map +1 -1
- package/lib/dist/activation/steering.d.ts +7 -4
- package/lib/dist/activation/steering.d.ts.map +1 -1
- package/lib/dist/activation/steering.js +10 -8
- package/lib/dist/activation/steering.js.map +1 -1
- package/lib/dist/agents/detect.d.ts +32 -0
- package/lib/dist/agents/detect.d.ts.map +1 -0
- package/lib/dist/agents/detect.js +54 -0
- package/lib/dist/agents/detect.js.map +1 -0
- package/lib/dist/agents/recent-project.d.ts +18 -0
- package/lib/dist/agents/recent-project.d.ts.map +1 -0
- package/lib/dist/agents/recent-project.js +92 -0
- package/lib/dist/agents/recent-project.js.map +1 -0
- package/lib/dist/analytics/canonical-tools.d.ts +23 -0
- package/lib/dist/analytics/canonical-tools.d.ts.map +1 -0
- package/lib/dist/analytics/canonical-tools.js +36 -0
- package/lib/dist/analytics/canonical-tools.js.map +1 -0
- package/lib/dist/analytics/canonical-tools.json +16 -0
- package/lib/dist/analytics/specship-impact.d.ts +3 -2
- package/lib/dist/analytics/specship-impact.d.ts.map +1 -1
- package/lib/dist/analytics/specship-impact.js +6 -4
- package/lib/dist/analytics/specship-impact.js.map +1 -1
- package/lib/dist/bin/specship.js +215 -170
- package/lib/dist/bin/specship.js.map +1 -1
- package/lib/dist/bin/uninstall.js +2 -2
- package/lib/dist/bin/uninstall.js.map +1 -1
- package/lib/dist/claudemd/audit.d.ts +12 -3
- package/lib/dist/claudemd/audit.d.ts.map +1 -1
- package/lib/dist/claudemd/audit.js +65 -22
- package/lib/dist/claudemd/audit.js.map +1 -1
- package/lib/dist/commands/specship/spec.md +9 -0
- package/lib/dist/db/migrations.d.ts +1 -1
- package/lib/dist/db/migrations.d.ts.map +1 -1
- package/lib/dist/db/migrations.js +18 -4
- package/lib/dist/db/migrations.js.map +1 -1
- package/lib/dist/db/schema.sql +8 -0
- package/lib/dist/db/spec-queries.d.ts +13 -1
- package/lib/dist/db/spec-queries.d.ts.map +1 -1
- package/lib/dist/db/spec-queries.js +21 -0
- package/lib/dist/db/spec-queries.js.map +1 -1
- package/lib/dist/enforce/changed-files.d.ts +7 -0
- package/lib/dist/enforce/changed-files.d.ts.map +1 -1
- package/lib/dist/enforce/changed-files.js +17 -0
- package/lib/dist/enforce/changed-files.js.map +1 -1
- package/lib/dist/enforce/enforce.d.ts +35 -1
- package/lib/dist/enforce/enforce.d.ts.map +1 -1
- package/lib/dist/enforce/enforce.js +66 -2
- package/lib/dist/enforce/enforce.js.map +1 -1
- package/lib/dist/extraction/index.d.ts +50 -4
- package/lib/dist/extraction/index.d.ts.map +1 -1
- package/lib/dist/extraction/index.js +105 -13
- package/lib/dist/extraction/index.js.map +1 -1
- package/lib/dist/extraction/parse-warnings.d.ts +16 -0
- package/lib/dist/extraction/parse-warnings.d.ts.map +1 -0
- package/lib/dist/extraction/parse-warnings.js +30 -0
- package/lib/dist/extraction/parse-warnings.js.map +1 -0
- package/lib/dist/extraction/tree-sitter.d.ts +27 -0
- package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.js +168 -1
- package/lib/dist/extraction/tree-sitter.js.map +1 -1
- package/lib/dist/health/smoke-check.js +1 -1
- package/lib/dist/health/smoke-check.js.map +1 -1
- package/lib/dist/hooks/hooks.json +16 -1
- package/lib/dist/index.d.ts +63 -3
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +235 -9
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/agent-record.d.ts +80 -0
- package/lib/dist/installer/agent-record.d.ts.map +1 -0
- package/lib/dist/installer/agent-record.js +351 -0
- package/lib/dist/installer/agent-record.js.map +1 -0
- package/lib/dist/installer/auto-migrate.d.ts +56 -0
- package/lib/dist/installer/auto-migrate.d.ts.map +1 -0
- package/lib/dist/installer/auto-migrate.js +328 -0
- package/lib/dist/installer/auto-migrate.js.map +1 -0
- package/lib/dist/installer/index.d.ts +37 -8
- package/lib/dist/installer/index.d.ts.map +1 -1
- package/lib/dist/installer/index.js +156 -46
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/installer/install-defaults.d.ts +4 -3
- package/lib/dist/installer/install-defaults.d.ts.map +1 -1
- package/lib/dist/installer/install-defaults.js +6 -5
- package/lib/dist/installer/install-defaults.js.map +1 -1
- package/lib/dist/installer/{config-writer.d.ts → targets/claude-config-writer.d.ts} +1 -1
- package/lib/dist/installer/targets/claude-config-writer.d.ts.map +1 -0
- package/lib/dist/installer/{config-writer.js → targets/claude-config-writer.js} +7 -6
- package/lib/dist/installer/targets/claude-config-writer.js.map +1 -0
- package/lib/dist/installer/targets/claude-projects.d.ts +9 -0
- package/lib/dist/installer/targets/claude-projects.d.ts.map +1 -0
- package/lib/dist/installer/targets/claude-projects.js +151 -0
- package/lib/dist/installer/targets/claude-projects.js.map +1 -0
- package/lib/dist/installer/targets/claude.d.ts +28 -1
- package/lib/dist/installer/targets/claude.d.ts.map +1 -1
- package/lib/dist/installer/targets/claude.js +39 -20
- package/lib/dist/installer/targets/claude.js.map +1 -1
- package/lib/dist/installer/targets/feature-parity.d.ts +28 -0
- package/lib/dist/installer/targets/feature-parity.d.ts.map +1 -0
- package/lib/dist/installer/targets/feature-parity.js +32 -0
- package/lib/dist/installer/targets/feature-parity.js.map +1 -0
- package/lib/dist/installer/{gemini-commands.d.ts → targets/gemini-commands.d.ts} +7 -3
- package/lib/dist/installer/targets/gemini-commands.d.ts.map +1 -0
- package/lib/dist/installer/{gemini-commands.js → targets/gemini-commands.js} +35 -14
- package/lib/dist/installer/targets/gemini-commands.js.map +1 -0
- package/lib/dist/installer/targets/gemini-projects.d.ts +11 -0
- package/lib/dist/installer/targets/gemini-projects.d.ts.map +1 -0
- package/lib/dist/installer/targets/gemini-projects.js +84 -0
- package/lib/dist/installer/targets/gemini-projects.js.map +1 -0
- package/lib/dist/installer/targets/gemini.d.ts +94 -8
- package/lib/dist/installer/targets/gemini.d.ts.map +1 -1
- package/lib/dist/installer/targets/gemini.js +392 -33
- package/lib/dist/installer/targets/gemini.js.map +1 -1
- package/lib/dist/installer/targets/registry.d.ts +8 -1
- package/lib/dist/installer/targets/registry.d.ts.map +1 -1
- package/lib/dist/installer/targets/registry.js +14 -5
- package/lib/dist/installer/targets/registry.js.map +1 -1
- package/lib/dist/installer/targets/shared.d.ts +44 -1
- package/lib/dist/installer/targets/shared.d.ts.map +1 -1
- package/lib/dist/installer/targets/shared.js +130 -3
- package/lib/dist/installer/targets/shared.js.map +1 -1
- package/lib/dist/installer/targets/types.d.ts +23 -1
- package/lib/dist/installer/targets/types.d.ts.map +1 -1
- package/lib/dist/jira/auto-publish.d.ts +22 -2
- package/lib/dist/jira/auto-publish.d.ts.map +1 -1
- package/lib/dist/jira/auto-publish.js +54 -9
- package/lib/dist/jira/auto-publish.js.map +1 -1
- package/lib/dist/jira/board-first.d.ts +15 -2
- package/lib/dist/jira/board-first.d.ts.map +1 -1
- package/lib/dist/jira/board-first.js +16 -0
- package/lib/dist/jira/board-first.js.map +1 -1
- package/lib/dist/jira/ci-publish.d.ts +72 -0
- package/lib/dist/jira/ci-publish.d.ts.map +1 -0
- package/lib/dist/jira/ci-publish.js +162 -0
- package/lib/dist/jira/ci-publish.js.map +1 -0
- package/lib/dist/jira/publish.d.ts +23 -0
- package/lib/dist/jira/publish.d.ts.map +1 -1
- package/lib/dist/jira/publish.js +0 -0
- package/lib/dist/jira/publish.js.map +1 -1
- package/lib/dist/jira/regression-pack.d.ts.map +1 -1
- package/lib/dist/jira/regression-pack.js +7 -1
- package/lib/dist/jira/regression-pack.js.map +1 -1
- package/lib/dist/jira/repo-config.d.ts.map +1 -1
- package/lib/dist/jira/repo-config.js +8 -0
- package/lib/dist/jira/repo-config.js.map +1 -1
- package/lib/dist/jira/types.d.ts +8 -0
- package/lib/dist/jira/types.d.ts.map +1 -1
- package/lib/dist/jira/types.js.map +1 -1
- package/lib/dist/knowledge/enforce.d.ts.map +1 -1
- package/lib/dist/knowledge/enforce.js +6 -1
- package/lib/dist/knowledge/enforce.js.map +1 -1
- package/lib/dist/mcp/jira-tools.d.ts +5 -0
- package/lib/dist/mcp/jira-tools.d.ts.map +1 -1
- package/lib/dist/mcp/jira-tools.js +59 -4
- package/lib/dist/mcp/jira-tools.js.map +1 -1
- package/lib/dist/mcp/proxy.d.ts.map +1 -1
- package/lib/dist/mcp/proxy.js +2 -1
- package/lib/dist/mcp/proxy.js.map +1 -1
- package/lib/dist/mcp/server-instructions.d.ts +16 -10
- package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
- package/lib/dist/mcp/server-instructions.js +69 -20
- package/lib/dist/mcp/server-instructions.js.map +1 -1
- package/lib/dist/mcp/session.d.ts +11 -0
- package/lib/dist/mcp/session.d.ts.map +1 -1
- package/lib/dist/mcp/session.js +43 -1
- package/lib/dist/mcp/session.js.map +1 -1
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +8 -0
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/reflect/apply.d.ts +2 -2
- package/lib/dist/reflect/apply.d.ts.map +1 -1
- package/lib/dist/reflect/apply.js +6 -5
- package/lib/dist/reflect/apply.js.map +1 -1
- package/lib/dist/reflect/miner.d.ts.map +1 -1
- package/lib/dist/reflect/miner.js +28 -15
- package/lib/dist/reflect/miner.js.map +1 -1
- package/lib/dist/reflect/session-outcomes.d.ts.map +1 -1
- package/lib/dist/reflect/session-outcomes.js +8 -3
- package/lib/dist/reflect/session-outcomes.js.map +1 -1
- package/lib/dist/reflect/targets.d.ts.map +1 -1
- package/lib/dist/reflect/targets.js +25 -5
- package/lib/dist/reflect/targets.js.map +1 -1
- package/lib/dist/reflect/types.d.ts +7 -0
- package/lib/dist/reflect/types.d.ts.map +1 -1
- package/lib/dist/resolution/spec-link-resolver.d.ts +14 -2
- package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/spec-link-resolver.js +13 -3
- package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
- package/lib/dist/server/canonical-tools.js +50 -0
- package/lib/dist/server/cli.js +2 -2
- package/lib/dist/server/host-profiles.js +39 -0
- package/lib/dist/server/ingest/index.js +2 -1
- package/lib/dist/server/ingest/ingestor.js +195 -76
- package/lib/dist/server/ingest/project-paths.js +8 -2
- package/lib/dist/server/ingest/recorded-projects.js +43 -0
- package/lib/dist/server/ingest/specship-classify.js +7 -2
- package/lib/dist/server/ingest/transcripts/claude-jsonl.js +54 -0
- package/lib/dist/server/ingest/transcripts/claude.js +64 -0
- package/lib/dist/server/ingest/transcripts/gemini.js +469 -0
- package/lib/dist/server/ingest/transcripts/index.js +14 -0
- package/lib/dist/server/ingest/{parser.js → transcripts/shared.js} +3 -49
- package/lib/dist/server/ingest/transcripts/types.js +4 -0
- package/lib/dist/server/ingest/watcher.js +31 -30
- package/lib/dist/server/routes/claude.js +27 -10
- package/lib/dist/server/routes/graph.js +64 -3
- package/lib/dist/server/routes/mcp.js +57 -15
- package/lib/dist/server/routes/memory.js +54 -35
- package/lib/dist/server/routes/projects.js +78 -7
- package/lib/dist/server/routes/spec.js +125 -0
- package/lib/dist/server/server.js +8 -2
- package/lib/dist/skills/spec-author/SKILL.md +1 -1
- package/lib/dist/skills/spec-author/references/gap-questions.md +1 -1
- package/lib/dist/spec-lint/lint.d.ts +26 -1
- package/lib/dist/spec-lint/lint.d.ts.map +1 -1
- package/lib/dist/spec-lint/lint.js +96 -7
- package/lib/dist/spec-lint/lint.js.map +1 -1
- package/lib/dist/sync/worktree.d.ts +48 -0
- package/lib/dist/sync/worktree.d.ts.map +1 -1
- package/lib/dist/sync/worktree.js +109 -0
- package/lib/dist/sync/worktree.js.map +1 -1
- package/lib/dist/ui/assets/bricolage-latin-C23fTOVi.woff2 +0 -0
- package/lib/dist/ui/assets/bricolage-latin-ext-B82pxkPf.woff2 +0 -0
- package/lib/dist/ui/assets/index-54vorjyq.css +1 -0
- package/lib/dist/ui/assets/index-BZQonCVb.js +52 -0
- package/lib/dist/ui/assets/space-grotesk-latin-DPT1xrvW.woff2 +0 -0
- package/lib/dist/ui/assets/space-grotesk-latin-ext-wJGaI8U0.woff2 +0 -0
- package/lib/dist/ui/index.html +2 -2
- package/lib/dist/update/updater.d.ts +6 -0
- package/lib/dist/update/updater.d.ts.map +1 -1
- package/lib/dist/update/updater.js +13 -1
- package/lib/dist/update/updater.js.map +1 -1
- package/lib/dist/workflows/agents/claude.d.ts +9 -0
- package/lib/dist/workflows/agents/claude.d.ts.map +1 -1
- package/lib/dist/workflows/agents/claude.js +22 -3
- package/lib/dist/workflows/agents/claude.js.map +1 -1
- package/lib/dist/workflows/agents/gemini-version.d.ts +18 -0
- package/lib/dist/workflows/agents/gemini-version.d.ts.map +1 -0
- package/lib/dist/workflows/agents/gemini-version.js +81 -0
- package/lib/dist/workflows/agents/gemini-version.js.map +1 -0
- package/lib/dist/workflows/agents/gemini.d.ts +1 -1
- package/lib/dist/workflows/agents/gemini.d.ts.map +1 -1
- package/lib/dist/workflows/agents/gemini.js +12 -2
- package/lib/dist/workflows/agents/gemini.js.map +1 -1
- package/lib/dist/workflows/agents/index.d.ts +2 -0
- package/lib/dist/workflows/agents/index.d.ts.map +1 -1
- package/lib/dist/workflows/agents/index.js +5 -0
- package/lib/dist/workflows/agents/index.js.map +1 -1
- package/lib/dist/workflows/agents/types.d.ts +13 -0
- package/lib/dist/workflows/agents/types.d.ts.map +1 -1
- package/lib/dist/workflows/agents/types.js.map +1 -1
- package/lib/dist/workflows/condition-evaluator.d.ts +8 -0
- package/lib/dist/workflows/condition-evaluator.d.ts.map +1 -1
- package/lib/dist/workflows/condition-evaluator.js +7 -0
- package/lib/dist/workflows/condition-evaluator.js.map +1 -1
- package/lib/dist/workflows/defaults/claude-design-implement.yaml +11 -11
- package/lib/dist/workflows/defaults/spec-author.yaml +9 -9
- package/lib/dist/workflows/defaults/spec-fix.yaml +8 -8
- package/lib/dist/workflows/defaults/spec-implement.yaml +38 -23
- package/lib/dist/workflows/defaults/spec-relink.yaml +5 -5
- package/lib/dist/workflows/defaults/spec-verify.yaml +3 -3
- package/lib/dist/workflows/executor.d.ts +7 -0
- package/lib/dist/workflows/executor.d.ts.map +1 -1
- package/lib/dist/workflows/executor.js +22 -5
- package/lib/dist/workflows/executor.js.map +1 -1
- package/lib/node_modules/.package-lock.json +1 -1
- package/lib/package.json +3 -2
- package/package.json +1 -1
- package/lib/dist/installer/config-writer.d.ts.map +0 -1
- package/lib/dist/installer/config-writer.js.map +0 -1
- package/lib/dist/installer/gemini-commands.d.ts.map +0 -1
- package/lib/dist/installer/gemini-commands.js.map +0 -1
- package/lib/dist/ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/lib/dist/ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/lib/dist/ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/lib/dist/ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/lib/dist/ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/lib/dist/ui/assets/index-BzuUWUAE.js +0 -50
- package/lib/dist/ui/assets/index-ezwX2EfG.css +0 -1
|
@@ -19,6 +19,22 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { writeSseHead } from './sse.js';
|
|
21
21
|
import { decodeProjectSlug, getLastIngestStats } from '../ingest/index.js';
|
|
22
|
+
import { CLAUDE_PROFILE, HOST_PROFILES } from '../host-profiles.js';
|
|
23
|
+
import { CANONICAL_TOOLS } from '../canonical-tools.js';
|
|
24
|
+
// Tool names in claude_tool_calls are the SHARED names every transcript
|
|
25
|
+
// adapter normalises to (REQ-AGENTSWITCH-004), so these queries cover every
|
|
26
|
+
// agent's sessions. Values come from our own canonical-tools.json, never from
|
|
27
|
+
// input, so quoting them into the SQL is safe.
|
|
28
|
+
const sqlString = (v) => `'${v.replace(/'/g, "''")}'`;
|
|
29
|
+
const FILE_TOUCH_TOOLS = [CANONICAL_TOOLS.read, CANONICAL_TOOLS.edit, CANONICAL_TOOLS.write, CANONICAL_TOOLS.notebookEdit];
|
|
30
|
+
const FILE_TOUCH_TOOLS_SQL = FILE_TOUCH_TOOLS.map(sqlString).join(',');
|
|
31
|
+
const FILE_TOUCH_TOOLS_SQL_WITH_MULTI = [...FILE_TOUCH_TOOLS, CANONICAL_TOOLS.multiEdit].map(sqlString).join(',');
|
|
32
|
+
const READ_TOOL_SQL = sqlString(CANONICAL_TOOLS.read);
|
|
33
|
+
/** `<configDir>/settings.json` of the agent a session came from (Claude Code when unknown). */
|
|
34
|
+
function settingsFileFor(host) {
|
|
35
|
+
const profile = HOST_PROFILES.find((p) => p.id === host) ?? CLAUDE_PROFILE;
|
|
36
|
+
return `${profile.configDir}/settings.json`;
|
|
37
|
+
}
|
|
22
38
|
/**
|
|
23
39
|
* Normalize a `?project=` filter value to the form stored in
|
|
24
40
|
* claude_sessions.project_path. The Sessions page (and any other UI
|
|
@@ -361,7 +377,7 @@ export async function registerClaudeRoutes(app) {
|
|
|
361
377
|
ORDER BY inner_tc.ts DESC LIMIT 1) as lastOp
|
|
362
378
|
FROM claude_tool_calls AS tc
|
|
363
379
|
WHERE tc.session_id = ?
|
|
364
|
-
AND tc.tool_name IN (
|
|
380
|
+
AND tc.tool_name IN (${FILE_TOUCH_TOOLS_SQL_WITH_MULTI})
|
|
365
381
|
AND tc.input_summary != ''
|
|
366
382
|
GROUP BY tc.input_summary
|
|
367
383
|
ORDER BY ops DESC
|
|
@@ -437,7 +453,7 @@ export async function registerClaudeRoutes(app) {
|
|
|
437
453
|
const files = db.prepare(`
|
|
438
454
|
SELECT input_summary as path, COUNT(*) as calls, SUM(result_length) as resultBytes
|
|
439
455
|
FROM claude_tool_calls
|
|
440
|
-
WHERE ts >= ? AND tool_name IN (
|
|
456
|
+
WHERE ts >= ? AND tool_name IN (${FILE_TOUCH_TOOLS_SQL}) AND input_summary != ''${inProject()}
|
|
441
457
|
GROUP BY input_summary
|
|
442
458
|
ORDER BY calls DESC
|
|
443
459
|
LIMIT 100
|
|
@@ -450,7 +466,7 @@ export async function registerClaudeRoutes(app) {
|
|
|
450
466
|
const trendRows = db.prepare(`
|
|
451
467
|
SELECT input_summary as path, CAST((ts - ?) / ? AS INTEGER) as bucket, COUNT(*) as calls
|
|
452
468
|
FROM claude_tool_calls
|
|
453
|
-
WHERE ts >= ? AND tool_name IN (
|
|
469
|
+
WHERE ts >= ? AND tool_name IN (${FILE_TOUCH_TOOLS_SQL}) AND input_summary != ''${inProject()}
|
|
454
470
|
GROUP BY input_summary, bucket
|
|
455
471
|
`).all(...withProject(trend7Start, dayMs, trend7Start));
|
|
456
472
|
const trendByPath = new Map();
|
|
@@ -518,7 +534,7 @@ export async function registerClaudeRoutes(app) {
|
|
|
518
534
|
MAX(t.ts) as lastTs, MIN(t.ts) as firstTs
|
|
519
535
|
FROM claude_tool_calls t
|
|
520
536
|
LEFT JOIN claude_sessions s ON s.id = t.session_id
|
|
521
|
-
WHERE t.input_summary = ? AND t.ts >= ? AND t.tool_name IN (
|
|
537
|
+
WHERE t.input_summary = ? AND t.ts >= ? AND t.tool_name IN (${FILE_TOUCH_TOOLS_SQL})
|
|
522
538
|
GROUP BY t.session_id
|
|
523
539
|
ORDER BY calls DESC
|
|
524
540
|
LIMIT 50
|
|
@@ -526,7 +542,7 @@ export async function registerClaudeRoutes(app) {
|
|
|
526
542
|
const byTool = db.prepare(`
|
|
527
543
|
SELECT tool_name as name, COUNT(*) as calls, COALESCE(SUM(result_length), 0) as bytes
|
|
528
544
|
FROM claude_tool_calls
|
|
529
|
-
WHERE input_summary = ? AND ts >= ? AND tool_name IN (
|
|
545
|
+
WHERE input_summary = ? AND ts >= ? AND tool_name IN (${FILE_TOUCH_TOOLS_SQL})
|
|
530
546
|
GROUP BY tool_name
|
|
531
547
|
ORDER BY calls DESC
|
|
532
548
|
`).all(path, since);
|
|
@@ -930,7 +946,7 @@ export async function registerClaudeRoutes(app) {
|
|
|
930
946
|
const wastefulReads = db.prepare(`
|
|
931
947
|
SELECT session_id, input_summary as file, COUNT(*) as n
|
|
932
948
|
FROM claude_tool_calls
|
|
933
|
-
WHERE tool_name =
|
|
949
|
+
WHERE tool_name = ${READ_TOOL_SQL} AND input_summary != '' AND ${inProject}
|
|
934
950
|
GROUP BY session_id, input_summary
|
|
935
951
|
HAVING n >= 10
|
|
936
952
|
ORDER BY n DESC
|
|
@@ -970,13 +986,13 @@ export async function registerClaudeRoutes(app) {
|
|
|
970
986
|
title: `${r.tool_name} returned ${Math.round(r.result_length / 1000)}k chars (~${Math.round(estTokens / 1000)}k tokens est.) — try a structural query`,
|
|
971
987
|
why: 'Tools that dump raw content into context are the dominant cost driver. A structural query returns just what the agent needs.',
|
|
972
988
|
evidence: { session: r.session_id, detail: `${r.tool_name}(${r.input_summary.slice(0, 100)}) → ${r.result_length} chars (~${estTokens} tokens est.)` },
|
|
973
|
-
fix: r.tool_name ===
|
|
989
|
+
fix: r.tool_name === CANONICAL_TOOLS.bash ? 'specship_search "<symbol>" instead of Bash(grep)' : 'specship_explore "<symbol or question>"',
|
|
974
990
|
saving: `~$${((estTokens / 1_000_000) * 15).toFixed(2)} est. on this call`,
|
|
975
991
|
});
|
|
976
992
|
}
|
|
977
993
|
// Rule 3: "cache miss rate" — sessions with > 10 prompts and cache_read_rate < 0.3.
|
|
978
994
|
const lowCache = db.prepare(`
|
|
979
|
-
SELECT s.id, s.total_cache_read_tokens as cr, s.total_input_tokens as ti, s.total_cache_creation_tokens as cw, s.prompt_count, s.last_model
|
|
995
|
+
SELECT s.id, s.total_cache_read_tokens as cr, s.total_input_tokens as ti, s.total_cache_creation_tokens as cw, s.prompt_count, s.last_model, s.host
|
|
980
996
|
FROM claude_sessions s
|
|
981
997
|
WHERE s.prompt_count >= 10
|
|
982
998
|
AND (s.total_input_tokens + s.total_cache_creation_tokens + s.total_cache_read_tokens) > 0
|
|
@@ -996,7 +1012,8 @@ export async function registerClaudeRoutes(app) {
|
|
|
996
1012
|
title: `Cache read rate is ${Math.round(rate * 100)}% on a ${r.prompt_count}-prompt session`,
|
|
997
1013
|
why: 'When the prompt prefix changes every turn, the 1h cache gets invalidated. Pinning a stable system-prompt prefix lets the cache absorb most of your input.',
|
|
998
1014
|
evidence: { session: r.id, detail: `cache_read=${(r.cr / 1_000_000).toFixed(2)}M / total=${(total / 1_000_000).toFixed(2)}M` },
|
|
999
|
-
|
|
1015
|
+
// The session's own agent's settings file (REQ-AGENTSWITCH-004).
|
|
1016
|
+
fix: `Pin a stable system-prompt prefix in ${settingsFileFor(r.host)}`,
|
|
1000
1017
|
// Not measurable per session without a model price table — state it
|
|
1001
1018
|
// qualitatively rather than render a placeholder (REQ-AUDFIX-006.A1).
|
|
1002
1019
|
saving: 'cached input bills at a fraction of fresh input',
|
|
@@ -1008,7 +1025,7 @@ export async function registerClaudeRoutes(app) {
|
|
|
1008
1025
|
// mixed-model workflow (fresh context per step + external verification).
|
|
1009
1026
|
const flounder = db.prepare(`
|
|
1010
1027
|
SELECT s.id, s.last_model,
|
|
1011
|
-
(SELECT COUNT(*) FROM claude_tool_calls t WHERE t.session_id = s.id AND t.tool_name =
|
|
1028
|
+
(SELECT COUNT(*) FROM claude_tool_calls t WHERE t.session_id = s.id AND t.tool_name = ${READ_TOOL_SQL}) AS reads
|
|
1012
1029
|
FROM claude_sessions s
|
|
1013
1030
|
WHERE s.project_path IN (?, ?)
|
|
1014
1031
|
AND (LOWER(COALESCE(s.last_model,'')) LIKE '%haiku%' OR LOWER(COALESCE(s.last_model,'')) LIKE '%sonnet%')
|
|
@@ -10,6 +10,30 @@
|
|
|
10
10
|
* The MCP layer's `specship_*` tools are markdown-formatted for agents;
|
|
11
11
|
* here we return raw JSON so the UI can render visually.
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* Longest source the split view returns for one symbol. `getCode` already
|
|
15
|
+
* slices the symbol's own startLine..endLine; this caps a huge class body so a
|
|
16
|
+
* single match can't turn into a whole-file dump. File nodes get no code at all.
|
|
17
|
+
*/
|
|
18
|
+
export const MAX_CODE_LINES = 400;
|
|
19
|
+
/** One symbol's verbatim source, capped to its own body (null when unavailable). */
|
|
20
|
+
async function symbolCode(cg, node) {
|
|
21
|
+
if (node.kind === 'file')
|
|
22
|
+
return { code: null, codeTruncated: false };
|
|
23
|
+
let code = null;
|
|
24
|
+
try {
|
|
25
|
+
code = await cg.getCode(node.id);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
code = null;
|
|
29
|
+
}
|
|
30
|
+
if (code == null)
|
|
31
|
+
return { code: null, codeTruncated: false };
|
|
32
|
+
const lines = code.split('\n');
|
|
33
|
+
if (lines.length <= MAX_CODE_LINES)
|
|
34
|
+
return { code, codeTruncated: false };
|
|
35
|
+
return { code: lines.slice(0, MAX_CODE_LINES).join('\n'), codeTruncated: true };
|
|
36
|
+
}
|
|
13
37
|
/** Returns the active SpecShip or sends a 409. */
|
|
14
38
|
async function resolveCg(app, req, reply) {
|
|
15
39
|
const cg = await app.activeCg(req);
|
|
@@ -89,14 +113,51 @@ export async function registerGraphRoutes(app) {
|
|
|
89
113
|
}
|
|
90
114
|
if (matches.length === 0)
|
|
91
115
|
return reply.code(404).send({ error: 'not found' });
|
|
92
|
-
const
|
|
116
|
+
const withCode = req.query.includeCode === '1' || req.query.includeCode === 'true';
|
|
117
|
+
const enriched = await Promise.all(matches.map(async (n) => {
|
|
93
118
|
const callers = cg.getCallers(n.id).map((e) => e.node);
|
|
94
119
|
const callees = cg.getCallees(n.id).map((e) => e.node);
|
|
95
120
|
const links = cg.getSpecQueries().getLinksByNode(n.id);
|
|
96
|
-
|
|
97
|
-
|
|
121
|
+
const base = { ...n, callers, callees, linkedSpecs: links };
|
|
122
|
+
return withCode ? { ...base, ...(await symbolCode(cg, n)) } : base;
|
|
123
|
+
}));
|
|
98
124
|
return { matches: enriched };
|
|
99
125
|
});
|
|
126
|
+
/**
|
|
127
|
+
* GET /api/graph/split-search?q= — the Graph split view's one search box
|
|
128
|
+
* (REQ-DASHSIMPLE-005): spec matches (an exact requirement id first, then
|
|
129
|
+
* the spec full-text index) and code-symbol matches, as two groups.
|
|
130
|
+
*/
|
|
131
|
+
app.get('/api/graph/split-search', async (req, reply) => {
|
|
132
|
+
const cg = await resolveCg(app, req, reply);
|
|
133
|
+
if (!cg)
|
|
134
|
+
return;
|
|
135
|
+
const q = (req.query.q ?? '').trim();
|
|
136
|
+
const limit = Math.min(Math.max(parseInt(req.query.limit ?? '8', 10) || 8, 1), 50);
|
|
137
|
+
if (!q)
|
|
138
|
+
return { specs: [], symbols: [] };
|
|
139
|
+
const sq = cg.getSpecQueries();
|
|
140
|
+
const specs = [];
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
const exact = sq.getSpecById(q) ?? sq.getSpecById(q.toUpperCase());
|
|
143
|
+
if (exact) {
|
|
144
|
+
seen.add(exact.id);
|
|
145
|
+
specs.push({ id: exact.id, title: exact.title, kind: exact.kind, snippet: '' });
|
|
146
|
+
}
|
|
147
|
+
for (const hit of sq.searchSpecs(q, limit)) {
|
|
148
|
+
if (seen.has(hit.spec.id))
|
|
149
|
+
continue;
|
|
150
|
+
seen.add(hit.spec.id);
|
|
151
|
+
specs.push({ id: hit.spec.id, title: hit.spec.title, kind: hit.spec.kind, snippet: hit.snippet });
|
|
152
|
+
}
|
|
153
|
+
// Spec nodes live in the code graph too; the specs group already covers them.
|
|
154
|
+
const symbols = cg.searchNodes(q, { limit: limit * 2 })
|
|
155
|
+
.map((r) => r.node)
|
|
156
|
+
.filter((n) => n.kind !== 'spec')
|
|
157
|
+
.slice(0, limit)
|
|
158
|
+
.map((n) => ({ id: n.id, name: n.name, kind: n.kind, filePath: n.filePath, startLine: n.startLine }));
|
|
159
|
+
return { specs: specs.slice(0, limit), symbols };
|
|
160
|
+
});
|
|
100
161
|
app.post('/api/graph/explore', async (req, reply) => {
|
|
101
162
|
const cg = await resolveCg(app, req, reply);
|
|
102
163
|
if (!cg)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP servers routes — inventory, live status, and control (REQ-DESKTOP-026).
|
|
3
3
|
*
|
|
4
|
-
* Three real sources, merged (
|
|
5
|
-
*
|
|
4
|
+
* Three real sources, merged (each supported agent's config surfaces are
|
|
5
|
+
* the inventory; per-agent file names come from `host-profiles.ts`,
|
|
6
|
+
* REQ-AGENTSWITCH-001 — Gemini CLI's user and project `settings.json`
|
|
7
|
+
* entries are listed read-only under `gemini:<name>` keys):
|
|
6
8
|
* 1. Claude Code's own config — `~/.claude.json` (global `mcpServers` +
|
|
7
9
|
* `projects[<root>].mcpServers`) and `<projectRoot>/.mcp.json`, the
|
|
8
10
|
* same files `specship install` writes. Disabled servers come from
|
|
@@ -30,12 +32,15 @@ import { spawn } from 'node:child_process';
|
|
|
30
32
|
import * as fs from 'node:fs';
|
|
31
33
|
import * as os from 'node:os';
|
|
32
34
|
import * as path from 'node:path';
|
|
35
|
+
import { CLAUDE_PROFILE, GEMINI_PROFILE } from '../host-profiles.js';
|
|
33
36
|
function getDb(cg) {
|
|
34
37
|
const anyCg = cg;
|
|
35
38
|
if (anyCg.db?.getDb)
|
|
36
39
|
return anyCg.db.getDb();
|
|
37
40
|
return anyCg.queries?.db;
|
|
38
41
|
}
|
|
42
|
+
/** Inventory key prefix for a Gemini CLI entry, so it never collides with a Claude Code one of the same name. */
|
|
43
|
+
export const GEMINI_KEY_PREFIX = GEMINI_PROFILE.id + ':';
|
|
39
44
|
/** Config-write failures the routes translate to HTTP statuses. */
|
|
40
45
|
export class McpConfigError extends Error {
|
|
41
46
|
statusCode;
|
|
@@ -61,7 +66,7 @@ function readJson(p) {
|
|
|
61
66
|
export function configuredServers(paths) {
|
|
62
67
|
const out = new Map();
|
|
63
68
|
const claudeJson = readJson(paths.claudeJsonPath);
|
|
64
|
-
const add = (name, cfg) => out.set(name, { name, ...cfg });
|
|
69
|
+
const add = (name, cfg) => out.set(name, { name, host: CLAUDE_PROFILE.id, ...cfg });
|
|
65
70
|
for (const [name, entry] of Object.entries((claudeJson?.['mcpServers'] ?? {}))) {
|
|
66
71
|
add(name, { scope: 'user', entry, configFile: paths.claudeJsonPath, disabled: false, container: 'user' });
|
|
67
72
|
}
|
|
@@ -77,12 +82,26 @@ export function configuredServers(paths) {
|
|
|
77
82
|
add(name, { scope: 'project', entry, configFile: paths.claudeJsonPath, disabled: true, container: 'project-local' });
|
|
78
83
|
}
|
|
79
84
|
const disabledMcpjson = new Set((proj?.['disabledMcpjsonServers'] ?? []));
|
|
80
|
-
const mcpJsonPath = path.join(paths.projectRoot,
|
|
85
|
+
const mcpJsonPath = path.join(paths.projectRoot, CLAUDE_PROFILE.projectMcpFile);
|
|
81
86
|
const mcpJson = readJson(mcpJsonPath);
|
|
82
87
|
for (const [name, entry] of Object.entries((mcpJson?.['mcpServers'] ?? {}))) {
|
|
83
88
|
add(name, { scope: 'project', entry, configFile: mcpJsonPath, disabled: disabledMcpjson.has(name), container: 'mcpjson' });
|
|
84
89
|
}
|
|
85
90
|
}
|
|
91
|
+
if (paths.geminiSettingsPath) {
|
|
92
|
+
// Gemini CLI: user settings, then the project's settings (project wins).
|
|
93
|
+
const files = [[paths.geminiSettingsPath, 'user']];
|
|
94
|
+
if (paths.projectRoot)
|
|
95
|
+
files.push([path.join(paths.projectRoot, GEMINI_PROFILE.projectMcpFile), 'project']);
|
|
96
|
+
for (const [file, scope] of files) {
|
|
97
|
+
const settings = readJson(file);
|
|
98
|
+
for (const [name, entry] of Object.entries((settings?.['mcpServers'] ?? {}))) {
|
|
99
|
+
out.set(GEMINI_KEY_PREFIX + name, {
|
|
100
|
+
name, scope, entry, configFile: file, disabled: false, container: 'gemini', host: GEMINI_PROFILE.id,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
86
105
|
return out;
|
|
87
106
|
}
|
|
88
107
|
/**
|
|
@@ -281,6 +300,9 @@ export function setServerEnabled(paths, name, enabled) {
|
|
|
281
300
|
throw new McpConfigError(`unknown MCP server "${name}"`, 404);
|
|
282
301
|
if (cfg.disabled === !enabled)
|
|
283
302
|
return cfg;
|
|
303
|
+
if (cfg.container === 'gemini') {
|
|
304
|
+
throw new McpConfigError(`${GEMINI_PROFILE.displayName} servers are edited in ${cfg.configFile}`, 400);
|
|
305
|
+
}
|
|
284
306
|
if (cfg.container === 'mcpjson') {
|
|
285
307
|
editJsonFile(paths.claudeJsonPath, (data) => {
|
|
286
308
|
const projects = (data['projects'] = data['projects'] ?? {});
|
|
@@ -335,16 +357,16 @@ export function addServer(paths, input) {
|
|
|
335
357
|
if (input.scope === 'project') {
|
|
336
358
|
if (!paths.projectRoot)
|
|
337
359
|
throw new McpConfigError('no project open — cannot write .mcp.json', 400);
|
|
338
|
-
const mcpJsonPath = path.join(paths.projectRoot,
|
|
360
|
+
const mcpJsonPath = path.join(paths.projectRoot, CLAUDE_PROFILE.projectMcpFile);
|
|
339
361
|
editJsonFile(mcpJsonPath, (data) => {
|
|
340
362
|
data['mcpServers'] = { ...(data['mcpServers'] ?? {}), [name]: entry };
|
|
341
363
|
});
|
|
342
|
-
return { name, scope: 'project', entry, configFile: mcpJsonPath, disabled: false, container: 'mcpjson' };
|
|
364
|
+
return { name, scope: 'project', entry, configFile: mcpJsonPath, disabled: false, container: 'mcpjson', host: CLAUDE_PROFILE.id };
|
|
343
365
|
}
|
|
344
366
|
editJsonFile(paths.claudeJsonPath, (data) => {
|
|
345
367
|
data['mcpServers'] = { ...(data['mcpServers'] ?? {}), [name]: entry };
|
|
346
368
|
});
|
|
347
|
-
return { name, scope: 'user', entry, configFile: paths.claudeJsonPath, disabled: false, container: 'user' };
|
|
369
|
+
return { name, scope: 'user', entry, configFile: paths.claudeJsonPath, disabled: false, container: 'user', host: CLAUDE_PROFILE.id };
|
|
348
370
|
}
|
|
349
371
|
/** Aggregate claude_tool_calls into per-server usage + the latest example call. */
|
|
350
372
|
function collectUsage(cg) {
|
|
@@ -354,16 +376,16 @@ function collectUsage(cg) {
|
|
|
354
376
|
try {
|
|
355
377
|
const db = getDb(cg);
|
|
356
378
|
usage = db.prepare(`
|
|
357
|
-
SELECT tool_name, COUNT(*) as calls,
|
|
379
|
+
SELECT host, tool_name, COUNT(*) as calls,
|
|
358
380
|
COALESCE(SUM(result_length), 0) as bytes,
|
|
359
381
|
MAX(ts) as last
|
|
360
382
|
FROM claude_tool_calls
|
|
361
383
|
WHERE tool_name LIKE 'mcp__%'
|
|
362
|
-
GROUP BY tool_name
|
|
384
|
+
GROUP BY host, tool_name
|
|
363
385
|
`).all();
|
|
364
386
|
// Latest verbatim inputs (schema v7) — first row per server wins below.
|
|
365
387
|
examples = db.prepare(`
|
|
366
|
-
SELECT tool_name, input_json
|
|
388
|
+
SELECT host, tool_name, input_json
|
|
367
389
|
FROM claude_tool_calls
|
|
368
390
|
WHERE tool_name LIKE 'mcp__%' AND input_json IS NOT NULL
|
|
369
391
|
ORDER BY ts DESC
|
|
@@ -372,11 +394,17 @@ function collectUsage(cg) {
|
|
|
372
394
|
}
|
|
373
395
|
catch { /* analytics tables absent — config-only response */ }
|
|
374
396
|
const parse = (t) => t.match(/^mcp__(.+?)__(.+)$/);
|
|
397
|
+
// Transcript adapters store every agent's MCP calls under the shared
|
|
398
|
+
// `mcp__<server>__<tool>` name (REQ-AGENTSWITCH-004), so the bare server
|
|
399
|
+
// name is the match key; a Gemini CLI call is credited to the Gemini entry
|
|
400
|
+
// (`gemini:<server>`) that inventory lists for it.
|
|
401
|
+
const keyFor = mcpUsageKey;
|
|
375
402
|
for (const u of usage) {
|
|
376
403
|
const m = parse(u.tool_name);
|
|
377
404
|
if (!m)
|
|
378
405
|
continue;
|
|
379
|
-
const [,
|
|
406
|
+
const [, bare, tool] = m;
|
|
407
|
+
const server = keyFor(u.host, bare);
|
|
380
408
|
if (!byServer.has(server))
|
|
381
409
|
byServer.set(server, { calls: 0, bytes: 0, last: 0, tools: new Map(), example: null });
|
|
382
410
|
const s = byServer.get(server);
|
|
@@ -395,7 +423,8 @@ function collectUsage(cg) {
|
|
|
395
423
|
const m = parse(row.tool_name);
|
|
396
424
|
if (!m)
|
|
397
425
|
continue;
|
|
398
|
-
const [,
|
|
426
|
+
const [, bare, tool] = m;
|
|
427
|
+
const server = keyFor(row.host, bare);
|
|
399
428
|
const s = byServer.get(server);
|
|
400
429
|
if (!s || s.example)
|
|
401
430
|
continue;
|
|
@@ -417,8 +446,9 @@ function collectUsage(cg) {
|
|
|
417
446
|
}
|
|
418
447
|
export async function registerMcpRoutes(app) {
|
|
419
448
|
const paths = () => ({
|
|
420
|
-
claudeJsonPath: path.join(os.homedir(),
|
|
449
|
+
claudeJsonPath: path.join(os.homedir(), CLAUDE_PROFILE.userConfigFile),
|
|
421
450
|
projectRoot: app.primaryCg?.getProjectRoot?.() ?? null,
|
|
451
|
+
geminiSettingsPath: path.join(os.homedir(), GEMINI_PROFILE.userConfigFile),
|
|
422
452
|
});
|
|
423
453
|
function requirePrimary(reply) {
|
|
424
454
|
if (!app.primaryCg) {
|
|
@@ -447,7 +477,8 @@ export async function registerMcpRoutes(app) {
|
|
|
447
477
|
const command = entry.url ?? [entry.command, ...(entry.args ?? [])].filter(Boolean).join(' ');
|
|
448
478
|
const state = deriveState(cfg, use?.last ? { lastMs: use.last } : null, probes.get(name) ?? null);
|
|
449
479
|
return {
|
|
450
|
-
id: name, name,
|
|
480
|
+
id: name, name: cfg?.name ?? name,
|
|
481
|
+
host: cfg?.host ?? null,
|
|
451
482
|
scope: cfg?.scope ?? 'unknown',
|
|
452
483
|
state, transport, command,
|
|
453
484
|
configFile: cfg?.configFile ?? null,
|
|
@@ -498,10 +529,21 @@ export async function registerMcpRoutes(app) {
|
|
|
498
529
|
}
|
|
499
530
|
});
|
|
500
531
|
}
|
|
501
|
-
/**
|
|
532
|
+
/**
|
|
533
|
+
* The inventory key a recorded MCP call is credited to: a Gemini CLI call to
|
|
534
|
+
* server `x` belongs to the `gemini:x` entry, any other agent's to `x`
|
|
535
|
+
* (REQ-AGENTSWITCH-004 — calls carry the shared name plus their host).
|
|
536
|
+
*/
|
|
537
|
+
export function mcpUsageKey(host, server) {
|
|
538
|
+
return host === GEMINI_PROFILE.id ? GEMINI_KEY_PREFIX + server : server;
|
|
539
|
+
}
|
|
540
|
+
/** True when transcripts contain any mcp__<name>__* call (a `gemini:` key matches Gemini CLI calls by bare server name). */
|
|
502
541
|
function hasUsage(cg, name) {
|
|
503
542
|
try {
|
|
504
543
|
const db = getDb(cg);
|
|
544
|
+
if (name.startsWith(GEMINI_KEY_PREFIX)) {
|
|
545
|
+
return db.prepare(`SELECT 1 FROM claude_tool_calls WHERE host = ? AND tool_name LIKE 'mcp__' || ? || '__%' LIMIT 1`).all(GEMINI_PROFILE.id, name.slice(GEMINI_KEY_PREFIX.length)).length > 0;
|
|
546
|
+
}
|
|
505
547
|
const rows = db.prepare(`SELECT 1 FROM claude_tool_calls WHERE tool_name LIKE 'mcp__' || ? || '__%' LIMIT 1`).all(name);
|
|
506
548
|
return rows.length > 0;
|
|
507
549
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GET /api/memory —
|
|
2
|
+
* GET /api/memory — every installed agent's memory-file hierarchy + notes.
|
|
3
3
|
*
|
|
4
|
-
* Walks
|
|
4
|
+
* Walks each agent's standard memory locations (from `host-profiles.ts`,
|
|
5
|
+
* REQ-AGENTSWITCH-001 — Claude Code's CLAUDE.md tree, Gemini CLI's GEMINI.md
|
|
6
|
+
* tree; each file is tagged with its `host`) and returns a flattened
|
|
5
7
|
* list of MemoryFile records the desktop UI can render directly. Resolves
|
|
6
8
|
* `@path` imports from each instruction file.
|
|
7
9
|
*
|
|
@@ -17,11 +19,7 @@ import { promises as fs } from 'node:fs';
|
|
|
17
19
|
import path from 'node:path';
|
|
18
20
|
import os from 'node:os';
|
|
19
21
|
import { decodeProjectSlug } from '../ingest/ingestor.js';
|
|
20
|
-
|
|
21
|
-
'/Library/Application Support/ClaudeCode/CLAUDE.md',
|
|
22
|
-
'/etc/ClaudeCode/CLAUDE.md',
|
|
23
|
-
process.env['ProgramData'] ? path.join(process.env['ProgramData'], 'ClaudeCode', 'CLAUDE.md') : '',
|
|
24
|
-
].filter(Boolean);
|
|
22
|
+
import { HOST_PROFILES } from '../host-profiles.js';
|
|
25
23
|
function estimateTokens(body) {
|
|
26
24
|
// Rough heuristic: ~4 chars per token. Cheap; the UI tolerates approximation.
|
|
27
25
|
return Math.max(1, Math.round(body.length / 4));
|
|
@@ -86,48 +84,56 @@ function makeFile(opts) {
|
|
|
86
84
|
body: opts.body,
|
|
87
85
|
readOnly: opts.readOnly,
|
|
88
86
|
imports: opts.imports?.length ? opts.imports : undefined,
|
|
87
|
+
host: opts.host,
|
|
89
88
|
};
|
|
90
89
|
}
|
|
91
|
-
|
|
90
|
+
/** Id suffix for a non-default host, so both agents' files stay distinct. */
|
|
91
|
+
function idSuffix(profile) {
|
|
92
|
+
return profile === HOST_PROFILES[0] ? '' : '-' + profile.id;
|
|
93
|
+
}
|
|
94
|
+
async function collectEnterprise(profile) {
|
|
92
95
|
const out = [];
|
|
93
|
-
for (const p of
|
|
96
|
+
for (const p of profile.enterpriseMemoryPaths()) {
|
|
94
97
|
const file = await tryRead(p);
|
|
95
98
|
if (file) {
|
|
96
99
|
out.push(makeFile({
|
|
97
|
-
id: 'm-ent',
|
|
100
|
+
id: 'm-ent' + idSuffix(profile),
|
|
98
101
|
level: 'enterprise',
|
|
99
102
|
type: 'instruction',
|
|
100
|
-
name:
|
|
103
|
+
name: profile.memoryFile,
|
|
101
104
|
scope: 'managed',
|
|
102
105
|
absolutePath: p,
|
|
103
106
|
body: file.body,
|
|
104
107
|
modifiedMs: file.modified,
|
|
105
108
|
readOnly: true,
|
|
109
|
+
host: profile.id,
|
|
106
110
|
}));
|
|
107
111
|
break;
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
114
|
return out;
|
|
111
115
|
}
|
|
112
|
-
async function collectUser() {
|
|
113
|
-
const p = path.join(os.homedir(),
|
|
116
|
+
async function collectUser(profile) {
|
|
117
|
+
const p = path.join(os.homedir(), profile.configDir, profile.memoryFile);
|
|
114
118
|
const file = await tryRead(p);
|
|
115
119
|
if (!file)
|
|
116
120
|
return [];
|
|
117
121
|
return [makeFile({
|
|
118
|
-
id: 'm-user',
|
|
122
|
+
id: 'm-user' + idSuffix(profile),
|
|
119
123
|
level: 'user',
|
|
120
124
|
type: 'instruction',
|
|
121
|
-
name:
|
|
122
|
-
scope: '
|
|
125
|
+
name: profile.memoryFile,
|
|
126
|
+
scope: '~/' + profile.configDir,
|
|
123
127
|
absolutePath: p,
|
|
124
128
|
body: file.body,
|
|
125
129
|
modifiedMs: file.modified,
|
|
130
|
+
host: profile.id,
|
|
126
131
|
})];
|
|
127
132
|
}
|
|
128
|
-
async function collectProject(projectRoot) {
|
|
133
|
+
async function collectProject(projectRoot, profile) {
|
|
129
134
|
const out = [];
|
|
130
|
-
const
|
|
135
|
+
const suffix = idSuffix(profile);
|
|
136
|
+
const proj = path.join(projectRoot, profile.memoryFile);
|
|
131
137
|
const projFile = await tryRead(proj);
|
|
132
138
|
const importMap = new Map();
|
|
133
139
|
if (projFile) {
|
|
@@ -137,7 +143,7 @@ async function collectProject(projectRoot) {
|
|
|
137
143
|
const abs = path.isAbsolute(imp) ? imp : path.join(projectRoot, imp);
|
|
138
144
|
const sub = await tryRead(abs);
|
|
139
145
|
if (sub) {
|
|
140
|
-
const id = 'm-imp-' + Buffer.from(imp).toString('base64').replace(/=/g, '').slice(0, 10);
|
|
146
|
+
const id = 'm-imp-' + Buffer.from(imp).toString('base64').replace(/=/g, '').slice(0, 10) + suffix;
|
|
141
147
|
importMap.set(imp, makeFile({
|
|
142
148
|
id,
|
|
143
149
|
level: 'import',
|
|
@@ -147,25 +153,27 @@ async function collectProject(projectRoot) {
|
|
|
147
153
|
absolutePath: abs,
|
|
148
154
|
body: sub.body,
|
|
149
155
|
modifiedMs: sub.modified,
|
|
156
|
+
host: profile.id,
|
|
150
157
|
}));
|
|
151
158
|
resolvedIds.push(id);
|
|
152
159
|
}
|
|
153
160
|
}
|
|
154
161
|
out.push(makeFile({
|
|
155
|
-
id: 'm-proj',
|
|
162
|
+
id: 'm-proj' + suffix,
|
|
156
163
|
level: 'project',
|
|
157
164
|
type: 'instruction',
|
|
158
|
-
name:
|
|
165
|
+
name: profile.memoryFile,
|
|
159
166
|
scope: 'project root',
|
|
160
167
|
absolutePath: proj,
|
|
161
168
|
body: projFile.body,
|
|
162
169
|
modifiedMs: projFile.modified,
|
|
163
170
|
imports: resolvedIds,
|
|
171
|
+
host: profile.id,
|
|
164
172
|
}));
|
|
165
173
|
for (const im of importMap.values())
|
|
166
174
|
out.push(im);
|
|
167
175
|
}
|
|
168
|
-
// Scan one level of subdirectories for nested
|
|
176
|
+
// Scan one level of subdirectories for nested memory files. Keeps the
|
|
169
177
|
// walk cheap — going deeper would need ignoring node_modules etc.
|
|
170
178
|
try {
|
|
171
179
|
const entries = await fs.readdir(projectRoot, { withFileTypes: true });
|
|
@@ -174,18 +182,19 @@ async function collectProject(projectRoot) {
|
|
|
174
182
|
continue;
|
|
175
183
|
if (ent.name.startsWith('.') || ent.name === 'node_modules' || ent.name === 'dist')
|
|
176
184
|
continue;
|
|
177
|
-
const subPath = path.join(projectRoot, ent.name,
|
|
185
|
+
const subPath = path.join(projectRoot, ent.name, profile.memoryFile);
|
|
178
186
|
const sub = await tryRead(subPath);
|
|
179
187
|
if (sub) {
|
|
180
188
|
out.push(makeFile({
|
|
181
|
-
id: 'm-sub-' + ent.name,
|
|
189
|
+
id: 'm-sub-' + ent.name + suffix,
|
|
182
190
|
level: 'subdir',
|
|
183
191
|
type: 'instruction',
|
|
184
|
-
name:
|
|
192
|
+
name: profile.memoryFile,
|
|
185
193
|
scope: ent.name,
|
|
186
194
|
absolutePath: subPath,
|
|
187
195
|
body: sub.body,
|
|
188
196
|
modifiedMs: sub.modified,
|
|
197
|
+
host: profile.id,
|
|
189
198
|
}));
|
|
190
199
|
}
|
|
191
200
|
}
|
|
@@ -195,9 +204,11 @@ async function collectProject(projectRoot) {
|
|
|
195
204
|
}
|
|
196
205
|
return out;
|
|
197
206
|
}
|
|
198
|
-
async function collectNotes() {
|
|
199
|
-
const dir = path.join(os.homedir(), '.claude', 'memory');
|
|
207
|
+
async function collectNotes(profile) {
|
|
200
208
|
const out = [];
|
|
209
|
+
if (!profile.notesDir)
|
|
210
|
+
return out;
|
|
211
|
+
const dir = path.join(os.homedir(), profile.notesDir);
|
|
201
212
|
let entries;
|
|
202
213
|
try {
|
|
203
214
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
@@ -217,7 +228,7 @@ async function collectNotes() {
|
|
|
217
228
|
.filter((t) => typeof t === 'string')
|
|
218
229
|
.slice(0, 6);
|
|
219
230
|
out.push(makeFile({
|
|
220
|
-
id: 'n-' + ent.name.replace(/\.md$/, ''),
|
|
231
|
+
id: 'n-' + ent.name.replace(/\.md$/, '') + idSuffix(profile),
|
|
221
232
|
level: 'note',
|
|
222
233
|
type: 'note',
|
|
223
234
|
name: ent.name,
|
|
@@ -225,6 +236,7 @@ async function collectNotes() {
|
|
|
225
236
|
absolutePath: abs,
|
|
226
237
|
body: file.body,
|
|
227
238
|
modifiedMs: file.modified,
|
|
239
|
+
host: profile.id,
|
|
228
240
|
}));
|
|
229
241
|
if (tags.length) {
|
|
230
242
|
const last = out[out.length - 1];
|
|
@@ -234,6 +246,19 @@ async function collectNotes() {
|
|
|
234
246
|
}
|
|
235
247
|
return out;
|
|
236
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Every agent's memory files, grouped tier by tier: enterprise, user, project,
|
|
251
|
+
* notes — each tier lists Claude Code's files, then Gemini CLI's.
|
|
252
|
+
*/
|
|
253
|
+
export async function collectMemoryFiles(projectRoot) {
|
|
254
|
+
const tiers = await Promise.all([
|
|
255
|
+
Promise.all(HOST_PROFILES.map((p) => collectEnterprise(p))),
|
|
256
|
+
Promise.all(HOST_PROFILES.map((p) => collectUser(p))),
|
|
257
|
+
projectRoot ? Promise.all(HOST_PROFILES.map((p) => collectProject(projectRoot, p))) : Promise.resolve([]),
|
|
258
|
+
Promise.all(HOST_PROFILES.map((p) => collectNotes(p))),
|
|
259
|
+
]);
|
|
260
|
+
return tiers.flat(2);
|
|
261
|
+
}
|
|
237
262
|
export async function registerMemoryRoutes(app) {
|
|
238
263
|
app.get('/api/memory', async (req) => {
|
|
239
264
|
// Memory has three global tiers (enterprise / user / notes) and one
|
|
@@ -251,13 +276,7 @@ export async function registerMemoryRoutes(app) {
|
|
|
251
276
|
projectRoot = req.query.projectPath;
|
|
252
277
|
else if (app.primaryCg?.getProjectRoot)
|
|
253
278
|
projectRoot = app.primaryCg.getProjectRoot();
|
|
254
|
-
const
|
|
255
|
-
collectEnterprise(),
|
|
256
|
-
collectUser(),
|
|
257
|
-
projectRoot ? collectProject(projectRoot) : Promise.resolve([]),
|
|
258
|
-
collectNotes(),
|
|
259
|
-
]);
|
|
260
|
-
const files = [...enterprise, ...user, ...project, ...notes];
|
|
279
|
+
const files = await collectMemoryFiles(projectRoot);
|
|
261
280
|
const resp = {
|
|
262
281
|
totalTokens: files.reduce((a, b) => a + b.tokens, 0),
|
|
263
282
|
instructionCount: files.filter((f) => f.type === 'instruction').length,
|