@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
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Transcript → SQLite ingestor, one adapter per agent (REQ-AGENTSWITCH-004).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* `ingestAll` walks every adapter in `./transcripts/` and ingests each of its
|
|
5
|
+
* files. Adapters normalise tool names to the shared set, so every row's
|
|
6
|
+
* `tool_name` is comparable across agents; `raw_tool_name` keeps the agent's
|
|
7
|
+
* own name and `host` names the agent. A document the adapter cannot parse
|
|
8
|
+
* is skipped and counted (`filesUnreadable`), never guessed at.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* JSON-lines adapters (Claude Code) are append-only, so we resume from the
|
|
11
|
+
* last byte offset on every pass — no re-parse of the whole file. Document
|
|
12
|
+
* adapters (Gemini CLI) rewrite the whole file, so a changed file replaces
|
|
13
|
+
* that session's rows.
|
|
14
|
+
*
|
|
15
|
+
* JSON-lines pipeline per file:
|
|
11
16
|
* 1. Read state from `claude_ingest_state` (offset, last_size). If file
|
|
12
17
|
* grew, open it and seek to offset; otherwise skip.
|
|
13
18
|
* 2. Parse each line with parseLine(). Group entries by promptId.
|
|
@@ -26,15 +31,10 @@ import * as fs from 'fs';
|
|
|
26
31
|
import * as os from 'os';
|
|
27
32
|
import * as path from 'path';
|
|
28
33
|
import { computeCost, resolvePricing } from './pricing.js';
|
|
29
|
-
import {
|
|
34
|
+
import { toEpochMs } from './transcripts/shared.js';
|
|
30
35
|
import { classifyToolCall } from './specship-classify.js';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* `~/.claude/projects/-Users-alice-projects-foo` → `/Users/alice/projects/foo`
|
|
34
|
-
*/
|
|
35
|
-
export function decodeProjectSlug(slug) {
|
|
36
|
-
return slug.startsWith('-') ? '/' + slug.slice(1).replace(/-/g, '/') : slug;
|
|
37
|
-
}
|
|
36
|
+
import { transcriptAdapters, decodeProjectSlug, listClaudeTranscriptFiles } from './transcripts/index.js';
|
|
37
|
+
export { decodeProjectSlug };
|
|
38
38
|
/**
|
|
39
39
|
* Build a predicate that tells whether a stored `project_path` belongs to the
|
|
40
40
|
* primary project, given the primary's REAL filesystem path.
|
|
@@ -54,38 +54,28 @@ export function primaryProjectMatcher(primaryRealPath) {
|
|
|
54
54
|
return (storedPath) => storedPath === primaryRealPath || storedPath === mangled;
|
|
55
55
|
}
|
|
56
56
|
/**
|
|
57
|
-
* List every JSONL file inside `<claudeRoot>/<slug>/<sessionId>.jsonl`.
|
|
58
|
-
*
|
|
57
|
+
* List every Claude Code JSONL file inside `<claudeRoot>/<slug>/<sessionId>.jsonl`.
|
|
58
|
+
* Kept for existing callers; the Claude transcript adapter owns the walk.
|
|
59
59
|
*/
|
|
60
60
|
export function listTranscriptFiles(claudeRoot) {
|
|
61
|
+
return listClaudeTranscriptFiles(claudeRoot);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The adapters an ingest pass runs, each with its root. Named roots win and
|
|
65
|
+
* restrict the pass to those adapters (see `IngestOptions.roots`).
|
|
66
|
+
*/
|
|
67
|
+
export function resolveIngestRoots(options = {}) {
|
|
68
|
+
const named = { ...(options.roots ?? {}) };
|
|
69
|
+
if (options.claudeRoot)
|
|
70
|
+
named['claude'] = options.claudeRoot;
|
|
71
|
+
const anyNamed = Object.values(named).some((r) => typeof r === 'string' && r.length > 0);
|
|
61
72
|
const out = [];
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
for (const p of projects) {
|
|
70
|
-
if (!p.isDirectory())
|
|
71
|
-
continue;
|
|
72
|
-
const dir = path.join(claudeRoot, p.name);
|
|
73
|
-
let files;
|
|
74
|
-
try {
|
|
75
|
-
files = fs.readdirSync(dir, { withFileTypes: true });
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
for (const f of files) {
|
|
81
|
-
if (f.isFile() && f.name.toLowerCase().endsWith('.jsonl')) {
|
|
82
|
-
out.push({
|
|
83
|
-
filePath: path.join(dir, f.name),
|
|
84
|
-
projectPath: decodeProjectSlug(p.name),
|
|
85
|
-
projectSlug: p.name,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
73
|
+
for (const adapter of options.adapters ?? transcriptAdapters()) {
|
|
74
|
+
const root = named[adapter.id];
|
|
75
|
+
if (root)
|
|
76
|
+
out.push({ adapter, root });
|
|
77
|
+
else if (!anyNamed)
|
|
78
|
+
out.push({ adapter, root: adapter.defaultRoot(os.homedir()) });
|
|
89
79
|
}
|
|
90
80
|
return out;
|
|
91
81
|
}
|
|
@@ -146,16 +136,16 @@ function readTail(filePath, fromOffset) {
|
|
|
146
136
|
}
|
|
147
137
|
}
|
|
148
138
|
/**
|
|
149
|
-
* Walk every transcript
|
|
150
|
-
*
|
|
139
|
+
* Walk every adapter's transcript files, ingest anything new, and return
|
|
140
|
+
* aggregate stats. Synchronous: the caller controls cadence.
|
|
151
141
|
*/
|
|
152
142
|
export function ingestAll(db, options = {}) {
|
|
153
143
|
const start = Date.now();
|
|
154
|
-
const claudeRoot = options.claudeRoot ?? path.join(os.homedir(), '.claude', 'projects');
|
|
155
144
|
const verbose = options.verbose ?? false;
|
|
156
145
|
const stats = {
|
|
157
146
|
filesScanned: 0,
|
|
158
147
|
filesSkipped: 0,
|
|
148
|
+
filesUnreadable: 0,
|
|
159
149
|
bytesIngested: 0,
|
|
160
150
|
linesParsed: 0,
|
|
161
151
|
linesSkipped: 0,
|
|
@@ -165,12 +155,24 @@ export function ingestAll(db, options = {}) {
|
|
|
165
155
|
durationMs: 0,
|
|
166
156
|
};
|
|
167
157
|
const pricing = loadPricing(db);
|
|
168
|
-
const files =
|
|
169
|
-
for (const
|
|
158
|
+
const files = [];
|
|
159
|
+
for (const { adapter, root } of resolveIngestRoots(options)) {
|
|
160
|
+
for (const file of adapter.listFiles(root, { projectPaths: options.projectPaths }))
|
|
161
|
+
files.push({ adapter, file });
|
|
162
|
+
}
|
|
163
|
+
for (const { adapter, file: f } of files) {
|
|
170
164
|
stats.filesScanned++;
|
|
171
165
|
try {
|
|
172
|
-
const wasIngested = ingestFile(db,
|
|
173
|
-
if (
|
|
166
|
+
const wasIngested = ingestFile(db, adapter, f, pricing, options);
|
|
167
|
+
if (wasIngested === 'unreadable') {
|
|
168
|
+
// Skipped and counted, never guessed at (REQ-AGENTSWITCH-004.A3).
|
|
169
|
+
stats.filesUnreadable++;
|
|
170
|
+
if (verbose) {
|
|
171
|
+
// eslint-disable-next-line no-console
|
|
172
|
+
console.error('[ingest] unreadable transcript skipped:', f.filePath);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (!wasIngested.modified) {
|
|
174
176
|
stats.filesSkipped++;
|
|
175
177
|
}
|
|
176
178
|
else {
|
|
@@ -202,7 +204,28 @@ let lastIngestStats = null;
|
|
|
202
204
|
export function getLastIngestStats() {
|
|
203
205
|
return lastIngestStats;
|
|
204
206
|
}
|
|
205
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Ingest one transcript through its adapter. Returns `'unreadable'` when a
|
|
209
|
+
* document adapter cannot parse the file — nothing is written for it.
|
|
210
|
+
*/
|
|
211
|
+
export function ingestFile(db, adapter, file, pricing, options) {
|
|
212
|
+
return adapter.format === 'document'
|
|
213
|
+
? ingestDocumentFile(db, adapter, file, pricing, options)
|
|
214
|
+
: ingestJsonlFile(db, adapter, file, pricing, options);
|
|
215
|
+
}
|
|
216
|
+
/** Resolve the graph once per file; best-effort, never aborts ingest. */
|
|
217
|
+
function resolveGraphFor(options, projectPath) {
|
|
218
|
+
if (!options.resolveGraph)
|
|
219
|
+
return null;
|
|
220
|
+
try {
|
|
221
|
+
return options.resolveGraph(projectPath);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function ingestJsonlFile(db, adapter, file, pricing, options) {
|
|
228
|
+
const { filePath, projectPath } = file;
|
|
206
229
|
// Load state.
|
|
207
230
|
const stateRow = db
|
|
208
231
|
.prepare('SELECT last_offset, file_size, session_id FROM claude_ingest_state WHERE file_path = ?')
|
|
@@ -257,18 +280,10 @@ function ingestFile(db, filePath, projectPath, pricing, options) {
|
|
|
257
280
|
// on every tool call. Returns null when no resolver is configured. The
|
|
258
281
|
// resolver can throw on a locked/corrupt index; that estimate is best-effort
|
|
259
282
|
// decoration and must NEVER abort transcript ingest, so degrade to null.
|
|
260
|
-
|
|
261
|
-
if (options.resolveGraph) {
|
|
262
|
-
try {
|
|
263
|
-
graph = options.resolveGraph(projectPath);
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
graph = null;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
283
|
+
const graph = resolveGraphFor(options, projectPath);
|
|
269
284
|
// Per-file ingest in a transaction.
|
|
270
285
|
const txn = db.transaction(() => {
|
|
271
|
-
return processLines(db, filePath, projectPath, completeLines, pricing, graph);
|
|
286
|
+
return processLines(db, adapter, filePath, projectPath, completeLines, pricing, graph);
|
|
272
287
|
});
|
|
273
288
|
const result = txn();
|
|
274
289
|
// Persist new offset.
|
|
@@ -294,16 +309,121 @@ function ingestFile(db, filePath, projectPath, pricing, options) {
|
|
|
294
309
|
toolCalls: result.toolCalls,
|
|
295
310
|
};
|
|
296
311
|
}
|
|
312
|
+
const INSERT_TOOL_CALL_SQL = `
|
|
313
|
+
INSERT INTO claude_tool_calls (prompt_id, session_id, assistant_uuid, tool_use_id, tool_name, input_summary, input_json, result_length, ts, is_specship, displaced_files, resolution, host, raw_tool_name)
|
|
314
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
315
|
+
`;
|
|
316
|
+
/**
|
|
317
|
+
* Ingest one whole-document transcript (Gemini CLI). The file is rewritten
|
|
318
|
+
* whole on every update, so a changed file replaces that session's prompt
|
|
319
|
+
* and tool-call rows in one transaction; an unchanged one (same size, not
|
|
320
|
+
* modified since the last pass) is skipped. An unparseable document writes
|
|
321
|
+
* nothing and records no state, so it is retried — and counted — every pass.
|
|
322
|
+
*/
|
|
323
|
+
function ingestDocumentFile(db, adapter, file, pricing, options) {
|
|
324
|
+
const { filePath, projectPath } = file;
|
|
325
|
+
const stat = fs.statSync(filePath);
|
|
326
|
+
const stateRow = db
|
|
327
|
+
.prepare('SELECT last_ingested_at, file_size FROM claude_ingest_state WHERE file_path = ?')
|
|
328
|
+
.get(filePath);
|
|
329
|
+
// mtimeMs carries sub-millisecond precision; last_ingested_at is whole ms.
|
|
330
|
+
if (stateRow && stateRow.file_size === stat.size && stateRow.last_ingested_at >= Math.floor(stat.mtimeMs)) {
|
|
331
|
+
return { modified: false, bytes: 0, lines: 0, linesSkipped: 0, prompts: 0, toolCalls: 0 };
|
|
332
|
+
}
|
|
333
|
+
const text = fs.readFileSync(filePath, 'utf-8');
|
|
334
|
+
const session = adapter.parseFile(text);
|
|
335
|
+
if (!session)
|
|
336
|
+
return 'unreadable';
|
|
337
|
+
const now = Date.now();
|
|
338
|
+
db.prepare(`
|
|
339
|
+
INSERT INTO claude_projects (path, name, first_seen, last_seen)
|
|
340
|
+
VALUES (?, ?, ?, ?)
|
|
341
|
+
ON CONFLICT(path) DO UPDATE SET last_seen = excluded.last_seen
|
|
342
|
+
`).run(projectPath, projectPath, now, now);
|
|
343
|
+
const graph = resolveGraphFor(options, projectPath);
|
|
344
|
+
const txn = db.transaction(() => {
|
|
345
|
+
const sid = session.sessionId;
|
|
346
|
+
const before = {
|
|
347
|
+
prompts: db.prepare('SELECT COUNT(*) AS n FROM claude_prompts WHERE session_id = ?').get(sid).n,
|
|
348
|
+
toolCalls: db.prepare('SELECT COUNT(*) AS n FROM claude_tool_calls WHERE session_id = ?').get(sid).n,
|
|
349
|
+
};
|
|
350
|
+
db.prepare('DELETE FROM claude_tool_calls WHERE session_id = ?').run(sid);
|
|
351
|
+
db.prepare('DELETE FROM claude_prompts WHERE session_id = ?').run(sid);
|
|
352
|
+
let ti = 0, to = 0, tcw = 0, tcr = 0, cost = 0, toolCalls = 0;
|
|
353
|
+
const promptCosts = session.prompts.map((p) => computeCost({
|
|
354
|
+
input_tokens: p.inputTokens,
|
|
355
|
+
output_tokens: p.outputTokens,
|
|
356
|
+
cache_creation_input_tokens: p.cacheCreationTokens,
|
|
357
|
+
cache_read_input_tokens: p.cacheReadTokens,
|
|
358
|
+
}, resolvePricing(p.model ?? session.model ?? undefined, pricing)));
|
|
359
|
+
session.prompts.forEach((p, i) => {
|
|
360
|
+
ti += p.inputTokens;
|
|
361
|
+
to += p.outputTokens;
|
|
362
|
+
tcw += p.cacheCreationTokens;
|
|
363
|
+
tcr += p.cacheReadTokens;
|
|
364
|
+
cost += promptCosts[i];
|
|
365
|
+
});
|
|
366
|
+
db.prepare(`
|
|
367
|
+
INSERT INTO claude_sessions (id, project_path, source_file, started_at, ended_at, prompt_count,
|
|
368
|
+
total_input_tokens, total_output_tokens, total_cache_creation_tokens, total_cache_read_tokens,
|
|
369
|
+
total_cost_usd, last_model, host)
|
|
370
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
371
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
372
|
+
ended_at = excluded.ended_at,
|
|
373
|
+
prompt_count = excluded.prompt_count,
|
|
374
|
+
total_input_tokens = excluded.total_input_tokens,
|
|
375
|
+
total_output_tokens = excluded.total_output_tokens,
|
|
376
|
+
total_cache_creation_tokens = excluded.total_cache_creation_tokens,
|
|
377
|
+
total_cache_read_tokens = excluded.total_cache_read_tokens,
|
|
378
|
+
total_cost_usd = excluded.total_cost_usd,
|
|
379
|
+
last_model = COALESCE(excluded.last_model, claude_sessions.last_model),
|
|
380
|
+
host = excluded.host
|
|
381
|
+
`).run(sid, projectPath, filePath, session.startedAt, session.endedAt, session.prompts.length, ti, to, tcw, tcr, cost, session.model, adapter.id);
|
|
382
|
+
const insPrompt = db.prepare(`
|
|
383
|
+
INSERT INTO claude_prompts (id, session_id, text, ts, leaf_uuid, model, input_tokens, output_tokens,
|
|
384
|
+
cache_creation_tokens, cache_read_tokens, cost_usd, is_sidechain, assistant_text, thinking_text)
|
|
385
|
+
VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
386
|
+
`);
|
|
387
|
+
const insToolCall = db.prepare(INSERT_TOOL_CALL_SQL);
|
|
388
|
+
session.prompts.forEach((p, i) => {
|
|
389
|
+
insPrompt.run(p.id, sid, p.text, p.ts, p.model, p.inputTokens, p.outputTokens, p.cacheCreationTokens, p.cacheReadTokens, promptCosts[i], p.assistantText, p.thinkingText);
|
|
390
|
+
for (const tc of p.toolCalls) {
|
|
391
|
+
const cls = classifyToolCall({ toolName: tc.name, inputJson: tc.inputJson, resultLength: tc.resultLength }, graph);
|
|
392
|
+
insToolCall.run(p.id, sid, p.id, tc.id, tc.name, tc.summary, tc.inputJson, tc.resultLength, tc.ts, cls.isSpecship, cls.displacedFiles, cls.resolution, adapter.id, tc.rawName);
|
|
393
|
+
toolCalls++;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
return {
|
|
397
|
+
prompts: Math.max(0, session.prompts.length - before.prompts),
|
|
398
|
+
toolCalls: Math.max(0, toolCalls - before.toolCalls),
|
|
399
|
+
};
|
|
400
|
+
});
|
|
401
|
+
const result = txn();
|
|
402
|
+
db.prepare(`
|
|
403
|
+
INSERT INTO claude_ingest_state (file_path, last_offset, last_ingested_at, file_size, session_id)
|
|
404
|
+
VALUES (?, ?, ?, ?, ?)
|
|
405
|
+
ON CONFLICT(file_path) DO UPDATE SET
|
|
406
|
+
last_offset = excluded.last_offset,
|
|
407
|
+
last_ingested_at = excluded.last_ingested_at,
|
|
408
|
+
file_size = excluded.file_size,
|
|
409
|
+
session_id = excluded.session_id
|
|
410
|
+
`).run(filePath, stat.size, now, stat.size, session.sessionId);
|
|
411
|
+
if (options.verbose) {
|
|
412
|
+
// eslint-disable-next-line no-console
|
|
413
|
+
console.error(`[ingest] ${path.basename(filePath)} (${adapter.id}): ${session.prompts.length} prompts`);
|
|
414
|
+
}
|
|
415
|
+
return { modified: true, bytes: stat.size, lines: 0, linesSkipped: 0, prompts: result.prompts, toolCalls: result.toolCalls };
|
|
416
|
+
}
|
|
297
417
|
/**
|
|
298
418
|
* Single-pass over the entries in this file batch. We need to relate
|
|
299
419
|
* tool_use blocks (in assistant entries) to their tool_result blocks (in
|
|
300
420
|
* the next user entry) so result_length is captured. Maintain a map of
|
|
301
421
|
* pending tool_use_id → { promptId, name, summary, ts } as we walk.
|
|
302
422
|
*/
|
|
303
|
-
function processLines(db, filePath, projectPath, completeLines, pricing, graph) {
|
|
423
|
+
function processLines(db, adapter, filePath, projectPath, completeLines, pricing, graph) {
|
|
304
424
|
const insSession = db.prepare(`
|
|
305
|
-
INSERT INTO claude_sessions (id, project_path, source_file, started_at, ended_at, prompt_count, last_model)
|
|
306
|
-
VALUES (?, ?, ?, ?, ?, 0,
|
|
425
|
+
INSERT INTO claude_sessions (id, project_path, source_file, started_at, ended_at, prompt_count, last_model, host)
|
|
426
|
+
VALUES (?, ?, ?, ?, ?, 0, ?, '${adapter.id}')
|
|
307
427
|
ON CONFLICT(id) DO UPDATE SET
|
|
308
428
|
ended_at = excluded.ended_at,
|
|
309
429
|
last_model = COALESCE(excluded.last_model, claude_sessions.last_model)
|
|
@@ -330,10 +450,7 @@ function processLines(db, filePath, projectPath, completeLines, pricing, graph)
|
|
|
330
450
|
cache_read_tokens = excluded.cache_read_tokens,
|
|
331
451
|
cost_usd = excluded.cost_usd
|
|
332
452
|
`);
|
|
333
|
-
const insToolCall = db.prepare(
|
|
334
|
-
INSERT INTO claude_tool_calls (prompt_id, session_id, assistant_uuid, tool_use_id, tool_name, input_summary, input_json, result_length, ts, is_specship, displaced_files, resolution)
|
|
335
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
336
|
-
`);
|
|
453
|
+
const insToolCall = db.prepare(INSERT_TOOL_CALL_SQL);
|
|
337
454
|
/**
|
|
338
455
|
* Append the assistant's text + thinking blocks from one assistant turn
|
|
339
456
|
* onto the prompt row. A single user prompt can span multiple assistant
|
|
@@ -388,7 +505,7 @@ function processLines(db, filePath, projectPath, completeLines, pricing, graph)
|
|
|
388
505
|
};
|
|
389
506
|
const pendingTools = new Map();
|
|
390
507
|
for (const raw of completeLines) {
|
|
391
|
-
const entry = parseLine(raw);
|
|
508
|
+
const entry = adapter.parseLine(raw);
|
|
392
509
|
if (!entry) {
|
|
393
510
|
// Blank lines aren't events; a non-empty line the parser couldn't
|
|
394
511
|
// classify is a coverage gap and must be counted (REQ-DASHINT-008).
|
|
@@ -404,7 +521,7 @@ function processLines(db, filePath, projectPath, completeLines, pricing, graph)
|
|
|
404
521
|
if (entry.type === 'user') {
|
|
405
522
|
// Bookkeep session row first.
|
|
406
523
|
insSession.run(sessionId, projectPath, filePath, ts, ts, null);
|
|
407
|
-
const text = extractUserPrompt(entry);
|
|
524
|
+
const text = adapter.extractUserPrompt(entry);
|
|
408
525
|
const promptId = entry.promptId ?? entry.uuid ?? null;
|
|
409
526
|
const isSidechain = entry.isSidechain ? 1 : 0;
|
|
410
527
|
if (promptId) {
|
|
@@ -435,11 +552,11 @@ function processLines(db, filePath, projectPath, completeLines, pricing, graph)
|
|
|
435
552
|
if (Array.isArray(content)) {
|
|
436
553
|
for (const block of content) {
|
|
437
554
|
if (block && block.type === 'tool_result' && block.tool_use_id) {
|
|
438
|
-
const len = toolResultLength(block);
|
|
555
|
+
const len = adapter.toolResultLength(block);
|
|
439
556
|
const pending = pendingTools.get(block.tool_use_id);
|
|
440
557
|
if (pending) {
|
|
441
558
|
const cls = classifyToolCall({ toolName: pending.toolName, inputJson: pending.inputJson, resultLength: len }, graph);
|
|
442
|
-
insToolCall.run(pending.promptId, pending.sessionId, pending.assistantUuid, block.tool_use_id, pending.toolName, pending.summary, pending.inputJson, len, pending.ts, cls.isSpecship, cls.displacedFiles, cls.resolution);
|
|
559
|
+
insToolCall.run(pending.promptId, pending.sessionId, pending.assistantUuid, block.tool_use_id, pending.toolName, pending.summary, pending.inputJson, len, pending.ts, cls.isSpecship, cls.displacedFiles, cls.resolution, adapter.id, pending.rawToolName);
|
|
443
560
|
toolCallsInserted++;
|
|
444
561
|
pendingTools.delete(block.tool_use_id);
|
|
445
562
|
}
|
|
@@ -506,12 +623,14 @@ function processLines(db, filePath, projectPath, completeLines, pricing, graph)
|
|
|
506
623
|
catch {
|
|
507
624
|
inputJson = null;
|
|
508
625
|
}
|
|
626
|
+
const toolName = adapter.normalizeToolName(block.name);
|
|
509
627
|
pendingTools.set(block.id, {
|
|
510
628
|
promptId,
|
|
511
629
|
sessionId,
|
|
512
630
|
assistantUuid,
|
|
513
|
-
toolName
|
|
514
|
-
|
|
631
|
+
toolName,
|
|
632
|
+
rawToolName: block.name,
|
|
633
|
+
summary: adapter.summarizeToolInput(toolName, block.input),
|
|
515
634
|
inputJson,
|
|
516
635
|
ts,
|
|
517
636
|
});
|
|
@@ -546,7 +665,7 @@ function processLines(db, filePath, projectPath, completeLines, pricing, graph)
|
|
|
546
665
|
for (const [toolUseId, pending] of pendingTools) {
|
|
547
666
|
// result_length=0 for pending/unmatched tools → classifyToolCall returns 'n/a' for specship.
|
|
548
667
|
const cls = classifyToolCall({ toolName: pending.toolName, inputJson: pending.inputJson, resultLength: 0 }, graph);
|
|
549
|
-
insToolCall.run(pending.promptId, pending.sessionId, pending.assistantUuid, toolUseId, pending.toolName, pending.summary, pending.inputJson, 0, pending.ts, cls.isSpecship, cls.displacedFiles, cls.resolution);
|
|
668
|
+
insToolCall.run(pending.promptId, pending.sessionId, pending.assistantUuid, toolUseId, pending.toolName, pending.summary, pending.inputJson, 0, pending.ts, cls.isSpecship, cls.displacedFiles, cls.resolution, adapter.id, pending.rawToolName);
|
|
550
669
|
toolCallsInserted++;
|
|
551
670
|
}
|
|
552
671
|
return {
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* one of its ancestors) whose encoding equals the slug is the real path.
|
|
15
15
|
* 3. The legacy lossy decode, preserving behavior for hyphen-free paths.
|
|
16
16
|
*/
|
|
17
|
+
import { CLAUDE_PROFILE } from '../host-profiles.js';
|
|
18
|
+
import { recordedProjects } from './recorded-projects.js';
|
|
17
19
|
import * as fs from 'fs';
|
|
18
20
|
import * as os from 'os';
|
|
19
21
|
import * as path from 'path';
|
|
@@ -128,13 +130,17 @@ function sniffCwd(claudeRoot, slug) {
|
|
|
128
130
|
* mtime change (REQ-SLUGRES-001).
|
|
129
131
|
*/
|
|
130
132
|
export function createSlugResolver(opts = {}) {
|
|
131
|
-
const claudeJsonPath = opts.claudeJsonPath ?? path.join(os.homedir(),
|
|
132
|
-
const claudeRoot = opts.claudeRoot ?? path.join(os.homedir(),
|
|
133
|
+
const claudeJsonPath = opts.claudeJsonPath ?? path.join(os.homedir(), CLAUDE_PROFILE.userConfigFile);
|
|
134
|
+
const claudeRoot = opts.claudeRoot ?? path.join(os.homedir(), CLAUDE_PROFILE.configDir, 'projects');
|
|
133
135
|
const sniffed = new Map();
|
|
134
136
|
return (slug) => {
|
|
135
137
|
const fromIndex = claudeJsonIndex(claudeJsonPath).get(slug);
|
|
136
138
|
if (fromIndex)
|
|
137
139
|
return fromIndex;
|
|
140
|
+
// A project SpecShip is installed in, by any agent (REQ-AGENTSWITCH-011).
|
|
141
|
+
const recorded = recordedProjects(opts.recordPath).find((p) => encodeProjectSlug(p.path) === slug);
|
|
142
|
+
if (recorded)
|
|
143
|
+
return recorded.path;
|
|
138
144
|
const cached = sniffed.get(slug);
|
|
139
145
|
if (cached)
|
|
140
146
|
return cached;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects SpecShip is installed in, from the installed-agents record
|
|
3
|
+
* (REQ-AGENTSWITCH-006) — the agent-neutral source of project paths
|
|
4
|
+
* (REQ-AGENTSWITCH-011). An agent whose session folders cannot be mapped back
|
|
5
|
+
* to a path (Gemini CLI names them by a one-way hash of it) still has its
|
|
6
|
+
* projects found here. SpecShip's own file, read directly: the server never
|
|
7
|
+
* imports the CLI package.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
/** `$SPECSHIP_INSTALL_DIR` or `~/.specship` — the same rule the CLI uses. */
|
|
13
|
+
export function installedAgentsRecordPath(env = process.env, home = os.homedir()) {
|
|
14
|
+
return path.join(env.SPECSHIP_INSTALL_DIR || path.join(home, '.specship'), 'installed-agents.json');
|
|
15
|
+
}
|
|
16
|
+
/** Every project-scoped entry in the record, one row per project. Never throws. */
|
|
17
|
+
export function recordedProjects(recordPath = installedAgentsRecordPath()) {
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(fs.readFileSync(recordPath, 'utf-8'));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const entries = parsed?.entries;
|
|
26
|
+
if (!Array.isArray(entries))
|
|
27
|
+
return [];
|
|
28
|
+
const byPath = new Map();
|
|
29
|
+
for (const e of entries) {
|
|
30
|
+
if (!e || typeof e !== 'object')
|
|
31
|
+
continue;
|
|
32
|
+
const { scope, projectRoot, agent, recordedAt } = e;
|
|
33
|
+
if (scope !== 'local' || typeof projectRoot !== 'string' || !projectRoot)
|
|
34
|
+
continue;
|
|
35
|
+
const at = typeof recordedAt === 'string' ? Date.parse(recordedAt) || 0 : 0;
|
|
36
|
+
const row = byPath.get(projectRoot) ?? { path: projectRoot, agents: [], recordedAtMs: 0 };
|
|
37
|
+
if (typeof agent === 'string' && !row.agents.includes(agent))
|
|
38
|
+
row.agents.push(agent);
|
|
39
|
+
row.recordedAtMs = Math.max(row.recordedAtMs, at);
|
|
40
|
+
byPath.set(projectRoot, row);
|
|
41
|
+
}
|
|
42
|
+
return [...byPath.values()];
|
|
43
|
+
}
|
|
@@ -10,11 +10,16 @@
|
|
|
10
10
|
* pure (no I/O, no deps), so duplication is cheap; `__tests__/specship-impact-parity.test.ts`
|
|
11
11
|
* asserts the two copies stay behaviourally identical.
|
|
12
12
|
*/
|
|
13
|
+
import { specshipToolPrefix } from '../canonical-tools.js';
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// Constants
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
|
-
/**
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Prefix shared by every (normalised) MCP tool routed through the specship
|
|
19
|
+
* server. Tool names reaching this module are already in the shared set —
|
|
20
|
+
* each transcript adapter normalises its agent's names first (REQ-AGENTSWITCH-004).
|
|
21
|
+
*/
|
|
22
|
+
const MCP_SPECSHIP_PREFIX = specshipToolPrefix;
|
|
18
23
|
/**
|
|
19
24
|
* Strip-prefix base names of SpecShip tools that return code-graph source.
|
|
20
25
|
* These are the tools a SpecShip call can displace a native Read/Grep with.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude transcript line format (REQ-AGENTSWITCH-017): converts one line of a
|
|
3
|
+
* Claude Code JSONL transcript into a
|
|
4
|
+
* structured ClaudeRawEntry, or null for unparseable / unrecognised lines.
|
|
5
|
+
*
|
|
6
|
+
* Tolerant: a malformed line never throws; the worker logs and skips it.
|
|
7
|
+
*/
|
|
8
|
+
export function parseLine(line) {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
if (!trimmed)
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
const obj = JSON.parse(trimmed);
|
|
14
|
+
if (!obj || typeof obj !== 'object' || typeof obj.type !== 'string') {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return obj;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Extract the user-visible prompt text from a user-entry message. */
|
|
24
|
+
export function extractUserPrompt(entry) {
|
|
25
|
+
// Some user entries store text at the top level; most use message.content.
|
|
26
|
+
if (typeof entry.text === 'string' && entry.text.length > 0)
|
|
27
|
+
return entry.text;
|
|
28
|
+
const content = entry.message?.content;
|
|
29
|
+
if (typeof content === 'string')
|
|
30
|
+
return content;
|
|
31
|
+
if (Array.isArray(content)) {
|
|
32
|
+
return content
|
|
33
|
+
.filter((b) => b && typeof b === 'object')
|
|
34
|
+
.map((b) => (b.type === 'text' && typeof b.text === 'string' ? b.text : ''))
|
|
35
|
+
.filter((t) => t.length > 0)
|
|
36
|
+
.join('\n')
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
/** Sum the character length of a tool_result content block. */
|
|
42
|
+
export function toolResultLength(block) {
|
|
43
|
+
if (!block)
|
|
44
|
+
return 0;
|
|
45
|
+
const c = block.content;
|
|
46
|
+
if (typeof c === 'string')
|
|
47
|
+
return c.length;
|
|
48
|
+
if (Array.isArray(c)) {
|
|
49
|
+
return c.reduce((sum, item) => sum + (typeof item?.text === 'string' ? item.text.length : 0), 0);
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
// The agent-neutral helpers live in ./shared; re-exported for this adapter's callers.
|
|
54
|
+
export { toEpochMs, summarizeToolInput } from './shared.js';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code transcript adapter (REQ-AGENTSWITCH-004).
|
|
3
|
+
*
|
|
4
|
+
* Sessions live in `~/.claude/projects/<slug>/<sessionId>.jsonl`, where
|
|
5
|
+
* `<slug>` is the slash-escaped absolute project path. Files are append-only
|
|
6
|
+
* JSON lines, so the ingestor resumes from the last byte offset.
|
|
7
|
+
*
|
|
8
|
+
* Claude Code's tool names ARE the shared set (`Read`, `Bash`,
|
|
9
|
+
* `mcp__<server>__<tool>` …), so normalisation is the identity — which is
|
|
10
|
+
* what keeps existing rows and counts unchanged (REQ-AGENTSWITCH-004.A2).
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { CLAUDE_PROFILE } from '../../host-profiles.js';
|
|
15
|
+
import { extractUserPrompt, parseLine, toolResultLength } from './claude-jsonl.js';
|
|
16
|
+
import { summarizeToolInput } from './shared.js';
|
|
17
|
+
/**
|
|
18
|
+
* Convert Claude's slash-escaped project dir name back into a real path.
|
|
19
|
+
* `-Users-alice-projects-foo` → `/Users/alice/projects/foo`
|
|
20
|
+
*/
|
|
21
|
+
export function decodeProjectSlug(slug) {
|
|
22
|
+
return slug.startsWith('-') ? '/' + slug.slice(1).replace(/-/g, '/') : slug;
|
|
23
|
+
}
|
|
24
|
+
/** Every `<root>/<slug>/<sessionId>.jsonl`, with the decoded project path. */
|
|
25
|
+
export function listClaudeTranscriptFiles(root) {
|
|
26
|
+
const out = [];
|
|
27
|
+
let projects;
|
|
28
|
+
try {
|
|
29
|
+
projects = fs.readdirSync(root, { withFileTypes: true });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
for (const p of projects) {
|
|
35
|
+
if (!p.isDirectory())
|
|
36
|
+
continue;
|
|
37
|
+
const dir = path.join(root, p.name);
|
|
38
|
+
let files;
|
|
39
|
+
try {
|
|
40
|
+
files = fs.readdirSync(dir, { withFileTypes: true });
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
for (const f of files) {
|
|
46
|
+
if (f.isFile() && f.name.toLowerCase().endsWith('.jsonl')) {
|
|
47
|
+
out.push({ filePath: path.join(dir, f.name), projectPath: decodeProjectSlug(p.name), projectSlug: p.name });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
export const claudeTranscriptAdapter = {
|
|
54
|
+
id: CLAUDE_PROFILE.id,
|
|
55
|
+
format: 'jsonl',
|
|
56
|
+
defaultRoot: (home) => path.join(home, CLAUDE_PROFILE.configDir, 'projects'),
|
|
57
|
+
listFiles: (root) => listClaudeTranscriptFiles(root),
|
|
58
|
+
watches: (fileName) => fileName.toLowerCase().endsWith('.jsonl'),
|
|
59
|
+
normalizeToolName: (raw) => raw,
|
|
60
|
+
parseLine,
|
|
61
|
+
summarizeToolInput,
|
|
62
|
+
extractUserPrompt,
|
|
63
|
+
toolResultLength,
|
|
64
|
+
};
|