@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
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI transcript adapter (REQ-AGENTSWITCH-004, DOM-SPECSHIP-007).
|
|
3
|
+
*
|
|
4
|
+
* Verified against @google/gemini-cli-core 0.28.0
|
|
5
|
+
* (services/chatRecordingService.js, core/geminiChat.js, config/storage.js):
|
|
6
|
+
*
|
|
7
|
+
* - a session is ONE pretty-printed JSON document at
|
|
8
|
+
* `~/.gemini/tmp/<project_hash>/chats/session-<timestamp>-<id8>.json`,
|
|
9
|
+
* rewritten whole on every update — never JSON lines;
|
|
10
|
+
* - `<project_hash>` is sha256(project root) in hex, so a project is
|
|
11
|
+
* recognised by hashing the roots the caller knows; otherwise the path is
|
|
12
|
+
* kept as `gemini:<hash>` (never guessed);
|
|
13
|
+
* - `messages[]` carry `type` ('user' | 'gemini' | 'info' | …), `content`
|
|
14
|
+
* (string or parts), and on gemini messages `toolCalls[]`
|
|
15
|
+
* (`{ id, name, args, result: responseParts | null, timestamp }`),
|
|
16
|
+
* `thoughts[]` (`{ subject, description }`), `tokens`
|
|
17
|
+
* (`{ input, output, cached, thoughts, … }`) and `model`;
|
|
18
|
+
* - an MCP tool is recorded under its bare name (`specship_explore`); the
|
|
19
|
+
* qualified `<server>__<tool>` form appears only on a name collision.
|
|
20
|
+
*
|
|
21
|
+
* Gemini CLI 0.33.0 changed both (REQ-AGENTSWITCH-018; verified against
|
|
22
|
+
* 0.67.0 services/chatRecordingService and config/storage):
|
|
23
|
+
*
|
|
24
|
+
* - a session is JSON Lines (`session-*.jsonl`): a metadata record
|
|
25
|
+
* (`sessionId`, `projectHash`, `startTime`, …, optionally `messages`),
|
|
26
|
+
* then message records (keyed by `id`, the last one for an id wins),
|
|
27
|
+
* `{ $set }` metadata updates (a `$set.messages` replaces the list),
|
|
28
|
+
* `{ $patch }` records (`content` and tool-call `result`s for an id,
|
|
29
|
+
* `updates[]`, `removeIds[]`, `orderIds[]`) and `{ $rewindTo }`;
|
|
30
|
+
* - the folder is a short project name from Gemini's project registry, so
|
|
31
|
+
* the project is resolved from the header's `projectHash` (still
|
|
32
|
+
* sha256 of the root);
|
|
33
|
+
* - MCP tools are named `mcp_<server>_<tool>`;
|
|
34
|
+
* - tool responses and Gemini's `<session_context>` / `<hook_context>`
|
|
35
|
+
* turns are also `user` messages (they open no prompt), and a prompt's
|
|
36
|
+
* own text is in `displayContent` when Gemini added context to it.
|
|
37
|
+
*
|
|
38
|
+
* Tool names are mapped onto the shared set; a document that does not parse,
|
|
39
|
+
* or has no `messages` array, is unreadable — `parseFile` returns null and
|
|
40
|
+
* the ingestor counts it.
|
|
41
|
+
*/
|
|
42
|
+
import * as crypto from 'crypto';
|
|
43
|
+
import * as fs from 'fs';
|
|
44
|
+
import * as path from 'path';
|
|
45
|
+
import { GEMINI_PROFILE } from '../../host-profiles.js';
|
|
46
|
+
import { CANONICAL_TOOLS, specshipServerName, canonicalMcpToolName } from '../../canonical-tools.js';
|
|
47
|
+
import { summarizeToolInput, toEpochMs } from './shared.js';
|
|
48
|
+
import { normalizeToolName, } from './types.js';
|
|
49
|
+
/** Gemini CLI 0.28.0 built-in tool names → shared names. */
|
|
50
|
+
export const GEMINI_TOOL_NAMES = Object.freeze({
|
|
51
|
+
read_file: CANONICAL_TOOLS.read,
|
|
52
|
+
read_many_files: CANONICAL_TOOLS.read,
|
|
53
|
+
run_shell_command: CANONICAL_TOOLS.bash,
|
|
54
|
+
grep_search: CANONICAL_TOOLS.grep,
|
|
55
|
+
glob: CANONICAL_TOOLS.glob,
|
|
56
|
+
write_file: CANONICAL_TOOLS.write,
|
|
57
|
+
replace: CANONICAL_TOOLS.edit,
|
|
58
|
+
});
|
|
59
|
+
/** Bare SpecShip tools all start with this (tools/mcp-tool.js records the server's own tool name). */
|
|
60
|
+
const bareSpecshipPrefix = `${specshipServerName}_`;
|
|
61
|
+
/** The collision-qualified form Gemini used before 0.33.0: `<server>__<tool>`. */
|
|
62
|
+
const QUALIFIED_RE = /^([A-Za-z0-9_-]+?)__(.+)$/;
|
|
63
|
+
/** The form Gemini uses from 0.33.0: `mcp_<server>_<tool>` (server names carry no `_`). */
|
|
64
|
+
const MCP_PREFIXED_RE = /^mcp_([^_]+)_(.+)$/;
|
|
65
|
+
/** Map one Gemini tool name onto the shared set. */
|
|
66
|
+
export function normalizeGeminiToolName(raw) {
|
|
67
|
+
const builtin = normalizeToolName(GEMINI_TOOL_NAMES, raw);
|
|
68
|
+
if (builtin !== raw)
|
|
69
|
+
return builtin;
|
|
70
|
+
const p = raw.match(MCP_PREFIXED_RE);
|
|
71
|
+
if (p)
|
|
72
|
+
return canonicalMcpToolName(p[1], p[2]);
|
|
73
|
+
const q = raw.match(QUALIFIED_RE);
|
|
74
|
+
if (q)
|
|
75
|
+
return canonicalMcpToolName(q[1], q[2]);
|
|
76
|
+
if (raw.startsWith(bareSpecshipPrefix))
|
|
77
|
+
return canonicalMcpToolName(specshipServerName, raw);
|
|
78
|
+
return raw;
|
|
79
|
+
}
|
|
80
|
+
/** Re-key Gemini args onto the shape the shared summariser reads. */
|
|
81
|
+
function summarize(raw, name, args) {
|
|
82
|
+
if (!args || typeof args !== 'object')
|
|
83
|
+
return '';
|
|
84
|
+
if (raw === 'read_many_files') {
|
|
85
|
+
const inc = args['include'] ?? args['paths'];
|
|
86
|
+
if (Array.isArray(inc))
|
|
87
|
+
return inc.filter((x) => typeof x === 'string').join(', ').slice(0, 400);
|
|
88
|
+
}
|
|
89
|
+
const file = args['file_path'] ?? args['absolute_path'] ?? args['path'];
|
|
90
|
+
const keyed = typeof file === 'string' && args['file_path'] === undefined ? { ...args, file_path: file } : args;
|
|
91
|
+
return summarizeToolInput(name, keyed);
|
|
92
|
+
}
|
|
93
|
+
/** Text carried by a Gemini content / result value (string, part, or part list). */
|
|
94
|
+
function textLength(v) {
|
|
95
|
+
if (v == null)
|
|
96
|
+
return 0;
|
|
97
|
+
if (typeof v === 'string')
|
|
98
|
+
return v.length;
|
|
99
|
+
if (Array.isArray(v))
|
|
100
|
+
return v.reduce((n, p) => n + textLength(p), 0);
|
|
101
|
+
if (typeof v === 'object') {
|
|
102
|
+
const o = v;
|
|
103
|
+
if (typeof o['text'] === 'string')
|
|
104
|
+
return o['text'].length;
|
|
105
|
+
const fr = o['functionResponse'];
|
|
106
|
+
if (fr && typeof fr === 'object') {
|
|
107
|
+
const out = fr.response?.['output'];
|
|
108
|
+
if (typeof out === 'string')
|
|
109
|
+
return out.length;
|
|
110
|
+
try {
|
|
111
|
+
return JSON.stringify(fr.response ?? null).length;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
function contentText(v) {
|
|
121
|
+
if (typeof v === 'string')
|
|
122
|
+
return v;
|
|
123
|
+
if (Array.isArray(v)) {
|
|
124
|
+
return v.map((p) => (p && typeof p === 'object' && typeof p.text === 'string' ? p.text : ''))
|
|
125
|
+
.filter(Boolean).join('\n').trim();
|
|
126
|
+
}
|
|
127
|
+
if (v && typeof v === 'object' && typeof v.text === 'string')
|
|
128
|
+
return v.text;
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
132
|
+
const isRec = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
133
|
+
const strProp = (o, k) => (typeof o[k] === 'string' ? o[k] : undefined);
|
|
134
|
+
/**
|
|
135
|
+
* Fold a JSON Lines session (Gemini CLI >= 0.33.0) into the single-document
|
|
136
|
+
* shape, applying records in order the way Gemini's own loader does.
|
|
137
|
+
* Malformed lines are skipped; null when no metadata record is found.
|
|
138
|
+
*/
|
|
139
|
+
export function foldGeminiJsonl(text) {
|
|
140
|
+
let meta = null;
|
|
141
|
+
const messages = new Map();
|
|
142
|
+
const setMessages = (list) => {
|
|
143
|
+
if (!Array.isArray(list))
|
|
144
|
+
return;
|
|
145
|
+
for (const m of list)
|
|
146
|
+
if (isRec(m) && strProp(m, 'id'))
|
|
147
|
+
messages.set(m['id'], m);
|
|
148
|
+
};
|
|
149
|
+
const patchOne = (patch) => {
|
|
150
|
+
const existing = messages.get(patch['id']);
|
|
151
|
+
if (!existing)
|
|
152
|
+
return;
|
|
153
|
+
if (patch['content'] !== undefined)
|
|
154
|
+
existing['content'] = patch['content'];
|
|
155
|
+
const calls = existing['toolCalls'];
|
|
156
|
+
if (Array.isArray(patch['toolCalls']) && Array.isArray(calls)) {
|
|
157
|
+
for (const tp of patch['toolCalls']) {
|
|
158
|
+
if (!isRec(tp) || !('result' in tp))
|
|
159
|
+
continue;
|
|
160
|
+
const tc = calls.find((c) => isRec(c) && c['id'] === tp['id']);
|
|
161
|
+
if (isRec(tc))
|
|
162
|
+
tc['result'] = tp['result'];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
for (const line of text.split('\n')) {
|
|
167
|
+
if (!line.trim())
|
|
168
|
+
continue;
|
|
169
|
+
let r;
|
|
170
|
+
try {
|
|
171
|
+
r = JSON.parse(line);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (!isRec(r))
|
|
177
|
+
continue;
|
|
178
|
+
const rewind = strProp(r, '$rewindTo');
|
|
179
|
+
if (rewind !== undefined) {
|
|
180
|
+
let found = false;
|
|
181
|
+
for (const id of [...messages.keys()]) {
|
|
182
|
+
if (id === rewind)
|
|
183
|
+
found = true;
|
|
184
|
+
if (found)
|
|
185
|
+
messages.delete(id);
|
|
186
|
+
}
|
|
187
|
+
if (!found)
|
|
188
|
+
messages.clear();
|
|
189
|
+
}
|
|
190
|
+
else if (isRec(r['$patch'])) {
|
|
191
|
+
const p = r['$patch'];
|
|
192
|
+
if (strProp(p, 'id'))
|
|
193
|
+
patchOne(p);
|
|
194
|
+
if (Array.isArray(p['updates']))
|
|
195
|
+
for (const u of p['updates'])
|
|
196
|
+
if (isRec(u) && strProp(u, 'id'))
|
|
197
|
+
patchOne(u);
|
|
198
|
+
if (Array.isArray(p['removeIds']))
|
|
199
|
+
for (const id of p['removeIds'])
|
|
200
|
+
if (typeof id === 'string')
|
|
201
|
+
messages.delete(id);
|
|
202
|
+
if (Array.isArray(p['orderIds'])) {
|
|
203
|
+
const ordered = p['orderIds'].filter((id) => typeof id === 'string' && messages.has(id));
|
|
204
|
+
const keep = new Set(ordered);
|
|
205
|
+
const entries = [...messages.entries()];
|
|
206
|
+
messages.clear();
|
|
207
|
+
for (const [id, m] of entries)
|
|
208
|
+
if (!keep.has(id))
|
|
209
|
+
messages.set(id, m);
|
|
210
|
+
for (const id of ordered)
|
|
211
|
+
messages.set(id, entries.find(([k]) => k === id)[1]);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (strProp(r, 'id')) {
|
|
215
|
+
messages.set(r['id'], r);
|
|
216
|
+
}
|
|
217
|
+
else if (isRec(r['$set'])) {
|
|
218
|
+
const set = r['$set'];
|
|
219
|
+
if (Array.isArray(set['messages'])) {
|
|
220
|
+
messages.clear();
|
|
221
|
+
setMessages(set['messages']);
|
|
222
|
+
}
|
|
223
|
+
meta = { ...(meta ?? {}), ...set };
|
|
224
|
+
}
|
|
225
|
+
else if (strProp(r, 'sessionId') && strProp(r, 'projectHash')) {
|
|
226
|
+
meta = { ...(meta ?? {}), ...r };
|
|
227
|
+
setMessages(r['messages']);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!meta)
|
|
231
|
+
return null;
|
|
232
|
+
return { ...meta, messages: [...messages.values()] };
|
|
233
|
+
}
|
|
234
|
+
/** Parse one Gemini session (a JSON document, or JSON Lines). Null when it cannot be read. */
|
|
235
|
+
export function parseGeminiSession(text) {
|
|
236
|
+
let doc;
|
|
237
|
+
try {
|
|
238
|
+
doc = JSON.parse(text);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
doc = null;
|
|
242
|
+
}
|
|
243
|
+
// A JSON Lines session — or one holding only its header line, which parses whole.
|
|
244
|
+
if (!isRec(doc) || !Array.isArray(doc['messages']))
|
|
245
|
+
doc = foldGeminiJsonl(text);
|
|
246
|
+
if (!doc || typeof doc !== 'object')
|
|
247
|
+
return null;
|
|
248
|
+
const d = doc;
|
|
249
|
+
if (typeof d.sessionId !== 'string' || !d.sessionId || !Array.isArray(d.messages))
|
|
250
|
+
return null;
|
|
251
|
+
const sessionId = d.sessionId;
|
|
252
|
+
const startedAt = typeof d.startTime === 'string' ? toEpochMs(d.startTime) : 0;
|
|
253
|
+
const prompts = [];
|
|
254
|
+
let current = null;
|
|
255
|
+
let lastModel = null;
|
|
256
|
+
let lastTs = startedAt;
|
|
257
|
+
const openPrompt = (id, text, ts) => {
|
|
258
|
+
const p = {
|
|
259
|
+
id, text, ts, model: null,
|
|
260
|
+
inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0,
|
|
261
|
+
assistantText: null, thinkingText: null, toolCalls: [],
|
|
262
|
+
};
|
|
263
|
+
prompts.push(p);
|
|
264
|
+
return p;
|
|
265
|
+
};
|
|
266
|
+
d.messages.forEach((m, i) => {
|
|
267
|
+
if (!m || typeof m !== 'object')
|
|
268
|
+
return;
|
|
269
|
+
const msg = m;
|
|
270
|
+
const ts = typeof msg['timestamp'] === 'string' ? toEpochMs(msg['timestamp']) : lastTs;
|
|
271
|
+
lastTs = Math.max(lastTs, ts);
|
|
272
|
+
const id = typeof msg['id'] === 'string' && msg['id'] ? msg['id'] : `${sessionId}:${i}`;
|
|
273
|
+
if (msg['type'] === 'user') {
|
|
274
|
+
// From 0.33.0 Gemini also records tool responses (no text parts) and
|
|
275
|
+
// its own `<session_context>` / `<hook_context>` turns as user
|
|
276
|
+
// messages; like Gemini's own resume logic, those start no prompt.
|
|
277
|
+
const said = contentText(msg['content']);
|
|
278
|
+
if (!said || /^<(session|hook)_context>/.test(said))
|
|
279
|
+
return;
|
|
280
|
+
current = openPrompt(id, contentText(msg['displayContent']) || said, ts);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (msg['type'] !== 'gemini')
|
|
284
|
+
return;
|
|
285
|
+
// A gemini turn before any user message still belongs to a prompt row.
|
|
286
|
+
const prompt = current ?? (current = openPrompt(`${sessionId}:prompt`, null, ts));
|
|
287
|
+
const model = typeof msg['model'] === 'string' && msg['model'] ? msg['model'] : null;
|
|
288
|
+
if (model) {
|
|
289
|
+
prompt.model = model;
|
|
290
|
+
lastModel = model;
|
|
291
|
+
}
|
|
292
|
+
const tokens = (msg['tokens'] ?? null);
|
|
293
|
+
if (tokens && typeof tokens === 'object') {
|
|
294
|
+
const cached = num(tokens['cached']);
|
|
295
|
+
prompt.inputTokens += Math.max(0, num(tokens['input']) - cached);
|
|
296
|
+
prompt.cacheReadTokens += cached;
|
|
297
|
+
prompt.outputTokens += num(tokens['output']) + num(tokens['thoughts']);
|
|
298
|
+
}
|
|
299
|
+
const said = contentText(msg['content']);
|
|
300
|
+
if (said)
|
|
301
|
+
prompt.assistantText = (prompt.assistantText ?? '') + said + '\n\n';
|
|
302
|
+
const thoughts = msg['thoughts'];
|
|
303
|
+
if (Array.isArray(thoughts)) {
|
|
304
|
+
const t = thoughts
|
|
305
|
+
.map((th) => (th && typeof th === 'object'
|
|
306
|
+
? [th.subject, th.description]
|
|
307
|
+
.filter((x) => typeof x === 'string' && x.length > 0).join(': ')
|
|
308
|
+
: ''))
|
|
309
|
+
.filter(Boolean).join('\n');
|
|
310
|
+
if (t)
|
|
311
|
+
prompt.thinkingText = (prompt.thinkingText ?? '') + t + '\n\n';
|
|
312
|
+
}
|
|
313
|
+
const calls = msg['toolCalls'];
|
|
314
|
+
if (!Array.isArray(calls))
|
|
315
|
+
return;
|
|
316
|
+
calls.forEach((c, j) => {
|
|
317
|
+
if (!c || typeof c !== 'object')
|
|
318
|
+
return;
|
|
319
|
+
const call = c;
|
|
320
|
+
const rawName = typeof call['name'] === 'string' ? call['name'] : '';
|
|
321
|
+
if (!rawName)
|
|
322
|
+
return;
|
|
323
|
+
const name = normalizeGeminiToolName(rawName);
|
|
324
|
+
const args = call['args'] && typeof call['args'] === 'object' ? call['args'] : undefined;
|
|
325
|
+
let inputJson = null;
|
|
326
|
+
try {
|
|
327
|
+
inputJson = JSON.stringify(call['args'] ?? null);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
inputJson = null;
|
|
331
|
+
}
|
|
332
|
+
const tc = {
|
|
333
|
+
id: typeof call['id'] === 'string' && call['id'] ? call['id'] : `${id}:${j}`,
|
|
334
|
+
rawName,
|
|
335
|
+
name,
|
|
336
|
+
summary: summarize(rawName, name, args),
|
|
337
|
+
inputJson,
|
|
338
|
+
resultLength: textLength(call['result']),
|
|
339
|
+
ts: typeof call['timestamp'] === 'string' ? toEpochMs(call['timestamp']) : ts,
|
|
340
|
+
};
|
|
341
|
+
prompt.toolCalls.push(tc);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
const endedAt = typeof d.lastUpdated === 'string' ? Math.max(toEpochMs(d.lastUpdated), lastTs) : lastTs;
|
|
345
|
+
return { sessionId, startedAt: startedAt || (prompts[0]?.ts ?? endedAt), endedAt, model: lastModel, prompts };
|
|
346
|
+
}
|
|
347
|
+
/** sha256(root) in hex — Gemini's project hash (config/storage.js getFilePathHash). */
|
|
348
|
+
export function geminiProjectHash(projectRoot) {
|
|
349
|
+
return crypto.createHash('sha256').update(projectRoot).digest('hex');
|
|
350
|
+
}
|
|
351
|
+
const isSessionFile = (name) => /\.jsonl?$/i.test(name);
|
|
352
|
+
/** The `projectHash` in a JSON Lines session's header, read from its first line. */
|
|
353
|
+
function headerProjectHash(file) {
|
|
354
|
+
let fd;
|
|
355
|
+
try {
|
|
356
|
+
fd = fs.openSync(file, 'r');
|
|
357
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
358
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
359
|
+
const first = buf.toString('utf-8', 0, n).split('\n', 1)[0] ?? '';
|
|
360
|
+
const rec = JSON.parse(first);
|
|
361
|
+
return isRec(rec) && typeof rec['projectHash'] === 'string' ? rec['projectHash'] : null;
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
if (fd !== undefined)
|
|
368
|
+
fs.closeSync(fd);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Every `<root>/<project>/chats/*.json[l]`. The project is resolved when a
|
|
373
|
+
* known root hashes to the folder name (before 0.33.0) or to the session
|
|
374
|
+
* header's `projectHash` (0.33.0 on); otherwise it is kept as `gemini:<id>`.
|
|
375
|
+
*/
|
|
376
|
+
export function listGeminiTranscriptFiles(root, ctx = {}) {
|
|
377
|
+
const byHash = new Map();
|
|
378
|
+
for (const p of ctx.projectPaths ?? [])
|
|
379
|
+
byHash.set(geminiProjectHash(p), p);
|
|
380
|
+
const out = [];
|
|
381
|
+
let hashes;
|
|
382
|
+
try {
|
|
383
|
+
hashes = fs.readdirSync(root, { withFileTypes: true });
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
for (const h of hashes) {
|
|
389
|
+
if (!h.isDirectory())
|
|
390
|
+
continue;
|
|
391
|
+
const chats = path.join(root, h.name, 'chats');
|
|
392
|
+
let files;
|
|
393
|
+
try {
|
|
394
|
+
files = fs.readdirSync(chats, { withFileTypes: true });
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const byDir = byHash.get(h.name);
|
|
400
|
+
for (const f of files) {
|
|
401
|
+
if (!f.isFile() || !isSessionFile(f.name))
|
|
402
|
+
continue;
|
|
403
|
+
const filePath = path.join(chats, f.name);
|
|
404
|
+
let projectPath = byDir;
|
|
405
|
+
if (!projectPath && f.name.toLowerCase().endsWith('.jsonl')) {
|
|
406
|
+
const hash = headerProjectHash(filePath);
|
|
407
|
+
if (hash)
|
|
408
|
+
projectPath = byHash.get(hash);
|
|
409
|
+
}
|
|
410
|
+
out.push({ filePath, projectPath: projectPath ?? `${GEMINI_PROFILE.id}:${h.name}` });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Projects in Gemini CLI's registry (`~/.gemini/projects.json`, 0.33.0+:
|
|
417
|
+
* `{ projects: { <root>: <short id> } }`), with the session count and newest
|
|
418
|
+
* session under `tmp/<short id>/chats` (REQ-AGENTSWITCH-019.A1).
|
|
419
|
+
*/
|
|
420
|
+
export function listGeminiProjects(home) {
|
|
421
|
+
const dir = path.join(home, GEMINI_PROFILE.configDir);
|
|
422
|
+
let projects;
|
|
423
|
+
try {
|
|
424
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, 'projects.json'), 'utf-8'));
|
|
425
|
+
if (!isRec(parsed) || !isRec(parsed['projects']))
|
|
426
|
+
return [];
|
|
427
|
+
projects = parsed['projects'];
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return [];
|
|
431
|
+
}
|
|
432
|
+
const out = [];
|
|
433
|
+
for (const [root, id] of Object.entries(projects)) {
|
|
434
|
+
if (!path.isAbsolute(root))
|
|
435
|
+
continue;
|
|
436
|
+
let sessionCount = 0;
|
|
437
|
+
let lastModifiedMs = 0;
|
|
438
|
+
if (typeof id === 'string' && id && !/[\\/]/.test(id)) {
|
|
439
|
+
const chats = path.join(dir, 'tmp', id, 'chats');
|
|
440
|
+
try {
|
|
441
|
+
for (const f of fs.readdirSync(chats, { withFileTypes: true })) {
|
|
442
|
+
if (!f.isFile() || !isSessionFile(f.name))
|
|
443
|
+
continue;
|
|
444
|
+
sessionCount++;
|
|
445
|
+
try {
|
|
446
|
+
lastModifiedMs = Math.max(lastModifiedMs, fs.statSync(path.join(chats, f.name)).mtimeMs);
|
|
447
|
+
}
|
|
448
|
+
catch { /* gone */ }
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch { /* no sessions yet */ }
|
|
452
|
+
}
|
|
453
|
+
out.push({ path: root, sessionCount, lastModifiedMs });
|
|
454
|
+
}
|
|
455
|
+
return out;
|
|
456
|
+
}
|
|
457
|
+
export const geminiTranscriptAdapter = {
|
|
458
|
+
id: GEMINI_PROFILE.id,
|
|
459
|
+
format: 'document',
|
|
460
|
+
defaultRoot: (home) => path.join(home, GEMINI_PROFILE.configDir, 'tmp'),
|
|
461
|
+
listFiles: listGeminiTranscriptFiles,
|
|
462
|
+
listProjects: listGeminiProjects,
|
|
463
|
+
watches: (fileName) => {
|
|
464
|
+
const parts = fileName.split(/[\\/]/);
|
|
465
|
+
return parts.length >= 2 && parts[parts.length - 2] === 'chats' && isSessionFile(fileName);
|
|
466
|
+
},
|
|
467
|
+
normalizeToolName: normalizeGeminiToolName,
|
|
468
|
+
parseFile: parseGeminiSession,
|
|
469
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript adapter registry (REQ-AGENTSWITCH-004) — one adapter per agent,
|
|
3
|
+
* Claude Code first (the default host). Mirrors `src/workflows/agents/index.ts`.
|
|
4
|
+
*/
|
|
5
|
+
import { claudeTranscriptAdapter } from './claude.js';
|
|
6
|
+
import { geminiTranscriptAdapter } from './gemini.js';
|
|
7
|
+
const ADAPTERS = [claudeTranscriptAdapter, geminiTranscriptAdapter];
|
|
8
|
+
/** Every transcript adapter. */
|
|
9
|
+
export function transcriptAdapters() {
|
|
10
|
+
return ADAPTERS;
|
|
11
|
+
}
|
|
12
|
+
export { claudeTranscriptAdapter, decodeProjectSlug, listClaudeTranscriptFiles } from './claude.js';
|
|
13
|
+
export { geminiTranscriptAdapter, normalizeGeminiToolName, parseGeminiSession, geminiProjectHash, GEMINI_TOOL_NAMES } from './gemini.js';
|
|
14
|
+
export { normalizeToolName } from './types.js';
|
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Tolerant: a malformed line never throws; the worker logs and skips it.
|
|
2
|
+
* Agent-neutral transcript helpers shared by every transcript adapter
|
|
3
|
+
* (REQ-AGENTSWITCH-017): timestamp normalisation and the one-line tool-input
|
|
4
|
+
* summary the heatmap, tips and reflect read.
|
|
6
5
|
*/
|
|
7
|
-
export function parseLine(line) {
|
|
8
|
-
const trimmed = line.trim();
|
|
9
|
-
if (!trimmed)
|
|
10
|
-
return null;
|
|
11
|
-
try {
|
|
12
|
-
const obj = JSON.parse(trimmed);
|
|
13
|
-
if (!obj || typeof obj !== 'object' || typeof obj.type !== 'string') {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
return obj;
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
6
|
/** Convert a timestamp field (ISO string or epoch number) to epoch ms. */
|
|
23
7
|
export function toEpochMs(ts) {
|
|
24
8
|
if (ts == null)
|
|
@@ -30,24 +14,6 @@ export function toEpochMs(ts) {
|
|
|
30
14
|
const parsed = Date.parse(ts);
|
|
31
15
|
return Number.isFinite(parsed) ? parsed : Date.now();
|
|
32
16
|
}
|
|
33
|
-
/** Extract the user-visible prompt text from a user-entry message. */
|
|
34
|
-
export function extractUserPrompt(entry) {
|
|
35
|
-
// Some user entries store text at the top level; most use message.content.
|
|
36
|
-
if (typeof entry.text === 'string' && entry.text.length > 0)
|
|
37
|
-
return entry.text;
|
|
38
|
-
const content = entry.message?.content;
|
|
39
|
-
if (typeof content === 'string')
|
|
40
|
-
return content;
|
|
41
|
-
if (Array.isArray(content)) {
|
|
42
|
-
return content
|
|
43
|
-
.filter((b) => b && typeof b === 'object')
|
|
44
|
-
.map((b) => (b.type === 'text' && typeof b.text === 'string' ? b.text : ''))
|
|
45
|
-
.filter((t) => t.length > 0)
|
|
46
|
-
.join('\n')
|
|
47
|
-
.trim();
|
|
48
|
-
}
|
|
49
|
-
return '';
|
|
50
|
-
}
|
|
51
17
|
/**
|
|
52
18
|
* For a tool_use block, build a short, format-aware input summary:
|
|
53
19
|
* - Read/Edit/Write → file path
|
|
@@ -90,15 +56,3 @@ export function summarizeToolInput(name, input) {
|
|
|
90
56
|
return '';
|
|
91
57
|
}
|
|
92
58
|
}
|
|
93
|
-
/** Sum the character length of a tool_result content block. */
|
|
94
|
-
export function toolResultLength(block) {
|
|
95
|
-
if (!block)
|
|
96
|
-
return 0;
|
|
97
|
-
const c = block.content;
|
|
98
|
-
if (typeof c === 'string')
|
|
99
|
-
return c.length;
|
|
100
|
-
if (Array.isArray(c)) {
|
|
101
|
-
return c.reduce((sum, item) => sum + (typeof item?.text === 'string' ? item.text.length : 0), 0);
|
|
102
|
-
}
|
|
103
|
-
return 0;
|
|
104
|
-
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Filesystem watcher that re-runs the ingestor on
|
|
2
|
+
* Filesystem watcher that re-runs the ingestor on transcript changes, one
|
|
3
|
+
* watched root per transcript adapter (REQ-AGENTSWITCH-004).
|
|
3
4
|
*
|
|
4
5
|
* Strategy:
|
|
5
6
|
* - One initial full pass on start (catches everything written while the
|
|
6
7
|
* watcher wasn't running — same offset state, no duplicates).
|
|
7
|
-
* - fs.watch on
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* - fs.watch on each adapter's transcript root, debounced 300ms — agents
|
|
9
|
+
* write a session incrementally, so we don't want to re-ingest on every
|
|
10
|
+
* keystroke.
|
|
10
11
|
* - On change, re-run ingestAll. The state table makes this cheap: only
|
|
11
12
|
* new bytes are read + parsed.
|
|
12
13
|
*/
|
|
13
14
|
import * as fs from 'fs';
|
|
14
|
-
import
|
|
15
|
-
import * as path from 'path';
|
|
16
|
-
import { ingestAll } from './ingestor.js';
|
|
15
|
+
import { ingestAll, resolveIngestRoots } from './ingestor.js';
|
|
17
16
|
export function startWatcher(db, options = {}) {
|
|
18
|
-
const claudeRoot = options.claudeRoot ?? path.join(os.homedir(), '.claude', 'projects');
|
|
19
17
|
const debounceMs = options.debounceMs ?? 300;
|
|
20
18
|
const verbose = options.verbose ?? false;
|
|
19
|
+
// One root per transcript adapter (REQ-AGENTSWITCH-004).
|
|
20
|
+
const targets = resolveIngestRoots(options);
|
|
21
21
|
let pending = null;
|
|
22
22
|
const triggerSoon = () => {
|
|
23
23
|
if (pending)
|
|
@@ -25,7 +25,7 @@ export function startWatcher(db, options = {}) {
|
|
|
25
25
|
pending = setTimeout(() => {
|
|
26
26
|
pending = null;
|
|
27
27
|
try {
|
|
28
|
-
ingestAll(db,
|
|
28
|
+
ingestAll(db, options);
|
|
29
29
|
}
|
|
30
30
|
catch (err) {
|
|
31
31
|
if (verbose) {
|
|
@@ -39,24 +39,25 @@ export function startWatcher(db, options = {}) {
|
|
|
39
39
|
if (options.initialIngest !== false) {
|
|
40
40
|
triggerSoon();
|
|
41
41
|
}
|
|
42
|
-
// fs.watch
|
|
43
|
-
// events; on Windows the recursive flag is supported as of recent Node.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
// fs.watch each adapter's root, recursive. On macOS + Linux this gives us
|
|
43
|
+
// per-file events; on Windows the recursive flag is supported as of recent Node.
|
|
44
|
+
const watchers = [];
|
|
45
|
+
for (const { adapter, root } of targets) {
|
|
46
|
+
try {
|
|
47
|
+
watchers.push(fs.watch(root, { recursive: true, persistent: true }, (_eventType, filename) => {
|
|
48
|
+
if (!filename)
|
|
49
|
+
return;
|
|
50
|
+
if (!adapter.watches(filename.toString()))
|
|
51
|
+
return;
|
|
52
|
+
triggerSoon();
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
// Directory doesn't exist (the agent was never used here) — just no-op.
|
|
57
|
+
if (verbose) {
|
|
58
|
+
// eslint-disable-next-line no-console
|
|
59
|
+
console.error('[ingest watcher] could not watch', root, ':', err instanceof Error ? err.message : String(err));
|
|
60
|
+
}
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
return {
|
|
@@ -65,13 +66,13 @@ export function startWatcher(db, options = {}) {
|
|
|
65
66
|
clearTimeout(pending);
|
|
66
67
|
pending = null;
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
+
for (const w of watchers) {
|
|
69
70
|
try {
|
|
70
|
-
|
|
71
|
+
w.close();
|
|
71
72
|
}
|
|
72
73
|
catch { /* ignore */ }
|
|
73
74
|
}
|
|
74
75
|
},
|
|
75
|
-
ingestNow: () => ingestAll(db,
|
|
76
|
+
ingestNow: () => ingestAll(db, options),
|
|
76
77
|
};
|
|
77
78
|
}
|