@mindfoldhq/trellis 0.6.0-beta.2 → 0.6.0-beta.20
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/README.md +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +58 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/channel/adapters/claude.d.ts +29 -0
- package/dist/commands/channel/adapters/claude.d.ts.map +1 -0
- package/dist/commands/channel/adapters/claude.js +203 -0
- package/dist/commands/channel/adapters/claude.js.map +1 -0
- package/dist/commands/channel/adapters/codex.d.ts +85 -0
- package/dist/commands/channel/adapters/codex.d.ts.map +1 -0
- package/dist/commands/channel/adapters/codex.js +505 -0
- package/dist/commands/channel/adapters/codex.js.map +1 -0
- package/dist/commands/channel/adapters/index.d.ts +84 -0
- package/dist/commands/channel/adapters/index.d.ts.map +1 -0
- package/dist/commands/channel/adapters/index.js +115 -0
- package/dist/commands/channel/adapters/index.js.map +1 -0
- package/dist/commands/channel/adapters/types.d.ts +33 -0
- package/dist/commands/channel/adapters/types.d.ts.map +1 -0
- package/dist/commands/channel/adapters/types.js +2 -0
- package/dist/commands/channel/adapters/types.js.map +1 -0
- package/dist/commands/channel/agent-loader.d.ts +32 -0
- package/dist/commands/channel/agent-loader.d.ts.map +1 -0
- package/dist/commands/channel/agent-loader.js +154 -0
- package/dist/commands/channel/agent-loader.js.map +1 -0
- package/dist/commands/channel/context-loader.d.ts +26 -0
- package/dist/commands/channel/context-loader.d.ts.map +1 -0
- package/dist/commands/channel/context-loader.js +290 -0
- package/dist/commands/channel/context-loader.js.map +1 -0
- package/dist/commands/channel/context.d.ts +16 -0
- package/dist/commands/channel/context.d.ts.map +1 -0
- package/dist/commands/channel/context.js +83 -0
- package/dist/commands/channel/context.js.map +1 -0
- package/dist/commands/channel/create.d.ts +27 -0
- package/dist/commands/channel/create.d.ts.map +1 -0
- package/dist/commands/channel/create.js +39 -0
- package/dist/commands/channel/create.js.map +1 -0
- package/dist/commands/channel/dev-parse-trace.d.ts +14 -0
- package/dist/commands/channel/dev-parse-trace.d.ts.map +1 -0
- package/dist/commands/channel/dev-parse-trace.js +70 -0
- package/dist/commands/channel/dev-parse-trace.js.map +1 -0
- package/dist/commands/channel/guard.d.ts +150 -0
- package/dist/commands/channel/guard.d.ts.map +1 -0
- package/dist/commands/channel/guard.js +474 -0
- package/dist/commands/channel/guard.js.map +1 -0
- package/dist/commands/channel/index.d.ts +3 -0
- package/dist/commands/channel/index.d.ts.map +1 -0
- package/dist/commands/channel/index.js +531 -0
- package/dist/commands/channel/index.js.map +1 -0
- package/dist/commands/channel/interrupt.d.ts +10 -0
- package/dist/commands/channel/interrupt.d.ts.map +1 -0
- package/dist/commands/channel/interrupt.js +22 -0
- package/dist/commands/channel/interrupt.js.map +1 -0
- package/dist/commands/channel/kill.d.ts +7 -0
- package/dist/commands/channel/kill.d.ts.map +1 -0
- package/dist/commands/channel/kill.js +121 -0
- package/dist/commands/channel/kill.js.map +1 -0
- package/dist/commands/channel/list.d.ts +17 -0
- package/dist/commands/channel/list.d.ts.map +1 -0
- package/dist/commands/channel/list.js +233 -0
- package/dist/commands/channel/list.js.map +1 -0
- package/dist/commands/channel/messages.d.ts +15 -0
- package/dist/commands/channel/messages.d.ts.map +1 -0
- package/dist/commands/channel/messages.js +245 -0
- package/dist/commands/channel/messages.js.map +1 -0
- package/dist/commands/channel/rm.d.ts +27 -0
- package/dist/commands/channel/rm.d.ts.map +1 -0
- package/dist/commands/channel/rm.js +216 -0
- package/dist/commands/channel/rm.js.map +1 -0
- package/dist/commands/channel/run.d.ts +30 -0
- package/dist/commands/channel/run.d.ts.map +1 -0
- package/dist/commands/channel/run.js +130 -0
- package/dist/commands/channel/run.js.map +1 -0
- package/dist/commands/channel/send.d.ts +11 -0
- package/dist/commands/channel/send.d.ts.map +1 -0
- package/dist/commands/channel/send.js +24 -0
- package/dist/commands/channel/send.js.map +1 -0
- package/dist/commands/channel/spawn.d.ts +40 -0
- package/dist/commands/channel/spawn.d.ts.map +1 -0
- package/dist/commands/channel/spawn.js +244 -0
- package/dist/commands/channel/spawn.js.map +1 -0
- package/dist/commands/channel/store/events.d.ts +39 -0
- package/dist/commands/channel/store/events.d.ts.map +1 -0
- package/dist/commands/channel/store/events.js +87 -0
- package/dist/commands/channel/store/events.js.map +1 -0
- package/dist/commands/channel/store/filter.d.ts +3 -0
- package/dist/commands/channel/store/filter.d.ts.map +1 -0
- package/dist/commands/channel/store/filter.js +2 -0
- package/dist/commands/channel/store/filter.js.map +1 -0
- package/dist/commands/channel/store/lock.d.ts +23 -0
- package/dist/commands/channel/store/lock.d.ts.map +1 -0
- package/dist/commands/channel/store/lock.js +99 -0
- package/dist/commands/channel/store/lock.js.map +1 -0
- package/dist/commands/channel/store/paths.d.ts +63 -0
- package/dist/commands/channel/store/paths.d.ts.map +1 -0
- package/dist/commands/channel/store/paths.js +246 -0
- package/dist/commands/channel/store/paths.js.map +1 -0
- package/dist/commands/channel/store/schema.d.ts +27 -0
- package/dist/commands/channel/store/schema.d.ts.map +1 -0
- package/dist/commands/channel/store/schema.js +34 -0
- package/dist/commands/channel/store/schema.js.map +1 -0
- package/dist/commands/channel/store/thread-state.d.ts +5 -0
- package/dist/commands/channel/store/thread-state.d.ts.map +1 -0
- package/dist/commands/channel/store/thread-state.js +16 -0
- package/dist/commands/channel/store/thread-state.js.map +1 -0
- package/dist/commands/channel/store/watch.d.ts +19 -0
- package/dist/commands/channel/store/watch.d.ts.map +1 -0
- package/dist/commands/channel/store/watch.js +146 -0
- package/dist/commands/channel/store/watch.js.map +1 -0
- package/dist/commands/channel/supervisor/idle.d.ts +46 -0
- package/dist/commands/channel/supervisor/idle.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/idle.js +72 -0
- package/dist/commands/channel/supervisor/idle.js.map +1 -0
- package/dist/commands/channel/supervisor/inbox.d.ts +30 -0
- package/dist/commands/channel/supervisor/inbox.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/inbox.js +160 -0
- package/dist/commands/channel/supervisor/inbox.js.map +1 -0
- package/dist/commands/channel/supervisor/shutdown.d.ts +68 -0
- package/dist/commands/channel/supervisor/shutdown.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/shutdown.js +146 -0
- package/dist/commands/channel/supervisor/shutdown.js.map +1 -0
- package/dist/commands/channel/supervisor/stdout.d.ts +51 -0
- package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/stdout.js +121 -0
- package/dist/commands/channel/supervisor/stdout.js.map +1 -0
- package/dist/commands/channel/supervisor/turns.d.ts +31 -0
- package/dist/commands/channel/supervisor/turns.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/turns.js +45 -0
- package/dist/commands/channel/supervisor/turns.js.map +1 -0
- package/dist/commands/channel/supervisor/warning.d.ts +48 -0
- package/dist/commands/channel/supervisor/warning.d.ts.map +1 -0
- package/dist/commands/channel/supervisor/warning.js +77 -0
- package/dist/commands/channel/supervisor/warning.js.map +1 -0
- package/dist/commands/channel/supervisor.d.ts +59 -0
- package/dist/commands/channel/supervisor.d.ts.map +1 -0
- package/dist/commands/channel/supervisor.js +344 -0
- package/dist/commands/channel/supervisor.js.map +1 -0
- package/dist/commands/channel/text-body.d.ts +13 -0
- package/dist/commands/channel/text-body.d.ts.map +1 -0
- package/dist/commands/channel/text-body.js +47 -0
- package/dist/commands/channel/text-body.js.map +1 -0
- package/dist/commands/channel/threads.d.ts +39 -0
- package/dist/commands/channel/threads.d.ts.map +1 -0
- package/dist/commands/channel/threads.js +106 -0
- package/dist/commands/channel/threads.js.map +1 -0
- package/dist/commands/channel/title.d.ts +12 -0
- package/dist/commands/channel/title.d.ts.map +1 -0
- package/dist/commands/channel/title.js +24 -0
- package/dist/commands/channel/title.js.map +1 -0
- package/dist/commands/channel/wait.d.ts +17 -0
- package/dist/commands/channel/wait.d.ts.map +1 -0
- package/dist/commands/channel/wait.js +75 -0
- package/dist/commands/channel/wait.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +97 -42
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mem.d.ts +13 -117
- package/dist/commands/mem.d.ts.map +1 -1
- package/dist/commands/mem.js +168 -1074
- package/dist/commands/mem.js.map +1 -1
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +28 -2
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +31 -111
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/upgrade.d.ts +28 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +84 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/commands/workflow.d.ts +35 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +219 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/configurators/claude.d.ts.map +1 -1
- package/dist/configurators/claude.js +1 -0
- package/dist/configurators/claude.js.map +1 -1
- package/dist/configurators/codex.d.ts.map +1 -1
- package/dist/configurators/codex.js +5 -3
- package/dist/configurators/codex.js.map +1 -1
- package/dist/configurators/shared.js +4 -4
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/workflow.d.ts +8 -0
- package/dist/configurators/workflow.d.ts.map +1 -1
- package/dist/configurators/workflow.js +3 -2
- package/dist/configurators/workflow.js.map +1 -1
- package/dist/migrations/manifests/0.5.10.json +9 -0
- package/dist/migrations/manifests/0.5.11.json +16 -0
- package/dist/migrations/manifests/0.5.12.json +9 -0
- package/dist/migrations/manifests/0.5.13.json +9 -0
- package/dist/migrations/manifests/0.5.14.json +9 -0
- package/dist/migrations/manifests/0.5.15.json +9 -0
- package/dist/migrations/manifests/0.5.16.json +9 -0
- package/dist/migrations/manifests/0.5.17.json +9 -0
- package/dist/migrations/manifests/0.5.18.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.10.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.11.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.12.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.13.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.14.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.15.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.16.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.17.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.18.json +16 -0
- package/dist/migrations/manifests/0.6.0-beta.19.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.20.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.3.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.4.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.5.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.6.json +16 -0
- package/dist/migrations/manifests/0.6.0-beta.7.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.8.json +9 -0
- package/dist/migrations/manifests/0.6.0-beta.9.json +9 -0
- package/dist/templates/claude/agents/trellis-check.md +13 -7
- package/dist/templates/claude/agents/trellis-implement.md +8 -7
- package/dist/templates/claude/settings.json +4 -4
- package/dist/templates/codebuddy/agents/trellis-check.md +13 -7
- package/dist/templates/codebuddy/agents/trellis-implement.md +8 -7
- package/dist/templates/codebuddy/settings.json +4 -4
- package/dist/templates/codex/agents/trellis-check.toml +4 -4
- package/dist/templates/codex/agents/trellis-implement.toml +4 -4
- package/dist/templates/codex/config.toml +5 -3
- package/dist/templates/codex/hooks/session-start.py +205 -119
- package/dist/templates/codex/hooks.json +2 -2
- package/dist/templates/codex/skills/before-dev/SKILL.md +12 -6
- package/dist/templates/codex/skills/brainstorm/SKILL.md +69 -457
- package/dist/templates/codex/skills/check/SKILL.md +86 -18
- package/dist/templates/codex/skills/start/SKILL.md +33 -323
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +7 -4
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +3 -2
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +5 -5
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +1 -1
- package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +35 -6
- package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +5 -4
- package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/SKILL.md +41 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/mcp-setup.md +90 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/repository-analysis.md +59 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/spec-task-planning.md +61 -0
- package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/spec-writing.md +70 -0
- package/dist/templates/common/commands/continue.md +6 -5
- package/dist/templates/common/commands/start.md +9 -6
- package/dist/templates/common/skills/before-dev.md +12 -6
- package/dist/templates/common/skills/brainstorm.md +68 -504
- package/dist/templates/common/skills/check.md +7 -1
- package/dist/templates/copilot/hooks/session-start.py +219 -101
- package/dist/templates/copilot/hooks.json +2 -2
- package/dist/templates/copilot/prompts/before-dev.prompt.md +12 -6
- package/dist/templates/copilot/prompts/brainstorm.prompt.md +69 -457
- package/dist/templates/copilot/prompts/check.prompt.md +86 -18
- package/dist/templates/copilot/prompts/parallel.prompt.md +16 -8
- package/dist/templates/copilot/prompts/start.prompt.md +33 -367
- package/dist/templates/cursor/agents/trellis-check.md +13 -7
- package/dist/templates/cursor/agents/trellis-implement.md +8 -7
- package/dist/templates/cursor/hooks.json +1 -7
- package/dist/templates/droid/droids/trellis-check.md +13 -7
- package/dist/templates/droid/droids/trellis-implement.md +8 -7
- package/dist/templates/droid/settings.json +4 -4
- package/dist/templates/gemini/agents/trellis-check.md +11 -5
- package/dist/templates/gemini/agents/trellis-implement.md +7 -6
- package/dist/templates/gemini/settings.json +2 -2
- package/dist/templates/kiro/agents/trellis-check.json +1 -1
- package/dist/templates/kiro/agents/trellis-implement.json +1 -1
- package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +127 -9
- package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +171 -6
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +333 -43
- package/dist/templates/markdown/spec/guides/index.md.txt +18 -0
- package/dist/templates/opencode/agents/trellis-check.md +13 -7
- package/dist/templates/opencode/agents/trellis-implement.md +9 -8
- package/dist/templates/opencode/lib/session-utils.js +212 -123
- package/dist/templates/opencode/lib/trellis-context.js +73 -11
- package/dist/templates/opencode/plugins/inject-subagent-context.js +131 -29
- package/dist/templates/opencode/plugins/inject-workflow-state.js +9 -5
- package/dist/templates/opencode/plugins/session-start.js +9 -1
- package/dist/templates/pi/agents/trellis-check.md +5 -4
- package/dist/templates/pi/agents/trellis-implement.md +5 -4
- package/dist/templates/pi/extensions/trellis/index.ts.txt +1357 -754
- package/dist/templates/qoder/agents/trellis-check.md +11 -5
- package/dist/templates/qoder/agents/trellis-implement.md +7 -6
- package/dist/templates/qoder/settings.json +4 -4
- package/dist/templates/shared-hooks/index.d.ts.map +1 -1
- package/dist/templates/shared-hooks/index.js +0 -1
- package/dist/templates/shared-hooks/index.js.map +1 -1
- package/dist/templates/shared-hooks/inject-subagent-context.py +36 -14
- package/dist/templates/shared-hooks/inject-workflow-state.py +40 -42
- package/dist/templates/shared-hooks/session-start.py +222 -171
- package/dist/templates/trellis/config.yaml +38 -0
- package/dist/templates/trellis/index.d.ts +1 -0
- package/dist/templates/trellis/index.d.ts.map +1 -1
- package/dist/templates/trellis/index.js +2 -0
- package/dist/templates/trellis/index.js.map +1 -1
- package/dist/templates/trellis/scripts/add_session.py +50 -24
- package/dist/templates/trellis/scripts/common/config.py +57 -1
- package/dist/templates/trellis/scripts/common/safe_commit.py +285 -0
- package/dist/templates/trellis/scripts/common/session_context.py +384 -137
- package/dist/templates/trellis/scripts/common/task_context.py +3 -3
- package/dist/templates/trellis/scripts/common/task_store.py +161 -15
- package/dist/templates/trellis/scripts/common/workflow_phase.py +7 -10
- package/dist/templates/trellis/scripts/task.py +3 -3
- package/dist/templates/trellis/workflow.md +119 -98
- package/dist/utils/cwd-guard.d.ts +38 -0
- package/dist/utils/cwd-guard.d.ts.map +1 -0
- package/dist/utils/cwd-guard.js +62 -0
- package/dist/utils/cwd-guard.js.map +1 -0
- package/dist/utils/file-writer.d.ts +13 -0
- package/dist/utils/file-writer.d.ts.map +1 -1
- package/dist/utils/file-writer.js +59 -1
- package/dist/utils/file-writer.js.map +1 -1
- package/dist/utils/manifest-prune.d.ts +61 -0
- package/dist/utils/manifest-prune.d.ts.map +1 -0
- package/dist/utils/manifest-prune.js +136 -0
- package/dist/utils/manifest-prune.js.map +1 -0
- package/dist/utils/task-json.d.ts +9 -42
- package/dist/utils/task-json.d.ts.map +1 -1
- package/dist/utils/task-json.js +8 -45
- package/dist/utils/task-json.js.map +1 -1
- package/dist/utils/template-hash.d.ts +32 -6
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +53 -31
- package/dist/utils/template-hash.js.map +1 -1
- package/dist/utils/uninstall-scrubbers.d.ts +1 -0
- package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
- package/dist/utils/uninstall-scrubbers.js +21 -0
- package/dist/utils/uninstall-scrubbers.js.map +1 -1
- package/dist/utils/workflow-resolver.d.ts +86 -0
- package/dist/utils/workflow-resolver.d.ts.map +1 -0
- package/dist/utils/workflow-resolver.js +265 -0
- package/dist/utils/workflow-resolver.js.map +1 -0
- package/package.json +9 -8
package/dist/commands/mem.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* mem.ts —
|
|
2
|
+
* mem.ts — CLI wrapper over `@mindfoldhq/trellis-core/mem`.
|
|
3
|
+
*
|
|
4
|
+
* The reusable retrieval / context-extraction logic lives in core; this file
|
|
5
|
+
* owns only CLI concerns: argument parsing, terminal rendering, the OpenCode
|
|
6
|
+
* "reader unavailable" notice, and process exit behavior.
|
|
3
7
|
*
|
|
4
8
|
* Commands:
|
|
5
9
|
* list list sessions (default if no command)
|
|
@@ -10,157 +14,9 @@
|
|
|
10
14
|
*
|
|
11
15
|
* Run `trellis mem help` for the full flag reference.
|
|
12
16
|
*/
|
|
13
|
-
import * as fs from "node:fs";
|
|
14
|
-
import * as path from "node:path";
|
|
15
17
|
import * as os from "node:os";
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
const PlatformSchema = z.enum(["claude", "codex", "opencode"]);
|
|
19
|
-
const SessionInfoSchema = z.object({
|
|
20
|
-
platform: PlatformSchema,
|
|
21
|
-
id: z.string(),
|
|
22
|
-
title: z.string().optional(),
|
|
23
|
-
cwd: z.string().optional(),
|
|
24
|
-
created: z.string().optional(),
|
|
25
|
-
updated: z.string().optional(),
|
|
26
|
-
filePath: z.string(),
|
|
27
|
-
messageDir: z.string().optional(),
|
|
28
|
-
parent_id: z.string().optional(), // OpenCode only: parent session id (sub-agent chain)
|
|
29
|
-
});
|
|
30
|
-
const DialogueRoleSchema = z.enum(["user", "assistant"]);
|
|
31
|
-
const SearchExcerptSchema = z.object({
|
|
32
|
-
role: DialogueRoleSchema,
|
|
33
|
-
snippet: z.string(),
|
|
34
|
-
});
|
|
35
|
-
const SearchHitSchema = z.object({
|
|
36
|
-
count: z.number(), // total token occurrences across all matching turns
|
|
37
|
-
user_count: z.number(), // breakdown: user-turn occurrences
|
|
38
|
-
asst_count: z.number(), // breakdown: assistant-turn occurrences
|
|
39
|
-
total_turns: z.number(), // size of cleaned dialogue (denominator for density)
|
|
40
|
-
excerpts: z.array(SearchExcerptSchema),
|
|
41
|
-
});
|
|
42
|
-
/** Weighted-density relevance score:
|
|
43
|
-
* (3 * user_hits + asst_hits) / total_turns
|
|
44
|
-
* Higher = the session is more topically concentrated on the query AND the
|
|
45
|
-
* user themselves brought it up (user hits weighted ×3 because the user's own
|
|
46
|
-
* words anchor "what they actually cared about", while assistant elaboration
|
|
47
|
-
* is downstream noise). */
|
|
48
|
-
export function relevanceScore(h) {
|
|
49
|
-
if (h.total_turns === 0)
|
|
50
|
-
return 0;
|
|
51
|
-
return (3 * h.user_count + h.asst_count) / h.total_turns;
|
|
52
|
-
}
|
|
53
|
-
const FilterSchema = z.object({
|
|
54
|
-
platform: z.union([PlatformSchema, z.literal("all")]),
|
|
55
|
-
since: z.date().optional(),
|
|
56
|
-
until: z.date().optional(),
|
|
57
|
-
cwd: z.string().optional(),
|
|
58
|
-
limit: z.number(),
|
|
59
|
-
});
|
|
60
|
-
const ArgvSchema = z.object({
|
|
61
|
-
cmd: z.string(),
|
|
62
|
-
positional: z.array(z.string()),
|
|
63
|
-
flags: z.record(z.string(), z.union([z.string(), z.boolean()])),
|
|
64
|
-
});
|
|
65
|
-
// ---------- schemas: external file formats ----------
|
|
66
|
-
// Claude Code JSONL events. We only declare the fields we read; everything
|
|
67
|
-
// else passes through. Content of an assistant `message` is an array of
|
|
68
|
-
// blocks (text / thinking / tool_use); content of a user `message` is a
|
|
69
|
-
// string for real human input or an array of tool_result blocks (skipped).
|
|
70
|
-
const ClaudeBlockSchema = z
|
|
71
|
-
.object({
|
|
72
|
-
type: z.string().optional(),
|
|
73
|
-
text: z.string().optional(),
|
|
74
|
-
})
|
|
75
|
-
.loose();
|
|
76
|
-
const ClaudeMessageSchema = z
|
|
77
|
-
.object({
|
|
78
|
-
role: z.string().optional(),
|
|
79
|
-
content: z.union([z.string(), z.array(ClaudeBlockSchema)]).optional(),
|
|
80
|
-
})
|
|
81
|
-
.loose();
|
|
82
|
-
const ClaudeEventSchema = z
|
|
83
|
-
.object({
|
|
84
|
-
type: z.string().optional(),
|
|
85
|
-
cwd: z.string().optional(),
|
|
86
|
-
timestamp: z.string().optional(),
|
|
87
|
-
message: ClaudeMessageSchema.optional(),
|
|
88
|
-
isCompactSummary: z.boolean().optional(),
|
|
89
|
-
})
|
|
90
|
-
.loose();
|
|
91
|
-
const ClaudeIndexEntrySchema = z
|
|
92
|
-
.object({
|
|
93
|
-
id: z.string(),
|
|
94
|
-
cwd: z.string().optional(),
|
|
95
|
-
created: z.string().optional(),
|
|
96
|
-
title: z.string().optional(),
|
|
97
|
-
})
|
|
98
|
-
.loose();
|
|
99
|
-
const ClaudeIndexSchema = z
|
|
100
|
-
.object({ entries: z.array(ClaudeIndexEntrySchema).optional() })
|
|
101
|
-
.loose();
|
|
102
|
-
// Codex rollout JSONL events.
|
|
103
|
-
const CodexContentPartSchema = z
|
|
104
|
-
.object({
|
|
105
|
-
type: z.string().optional(),
|
|
106
|
-
text: z.string().optional(),
|
|
107
|
-
})
|
|
108
|
-
.loose();
|
|
109
|
-
const CodexCompactedItemSchema = z
|
|
110
|
-
.object({
|
|
111
|
-
type: z.string().optional(),
|
|
112
|
-
role: z.string().optional(),
|
|
113
|
-
content: z.array(CodexContentPartSchema).optional(),
|
|
114
|
-
})
|
|
115
|
-
.loose();
|
|
116
|
-
const CodexPayloadSchema = z
|
|
117
|
-
.object({
|
|
118
|
-
type: z.string().optional(),
|
|
119
|
-
role: z.string().optional(),
|
|
120
|
-
cwd: z.string().optional(),
|
|
121
|
-
id: z.string().optional(),
|
|
122
|
-
content: z.array(CodexContentPartSchema).optional(),
|
|
123
|
-
replacement_history: z.array(CodexCompactedItemSchema).optional(),
|
|
124
|
-
})
|
|
125
|
-
.loose();
|
|
126
|
-
const CodexEventSchema = z
|
|
127
|
-
.object({
|
|
128
|
-
timestamp: z.string().optional(),
|
|
129
|
-
type: z.string().optional(),
|
|
130
|
-
payload: CodexPayloadSchema.optional(),
|
|
131
|
-
})
|
|
132
|
-
.loose();
|
|
133
|
-
// OpenCode session/message/part files.
|
|
134
|
-
const OpenCodeSessionSchema = z
|
|
135
|
-
.object({
|
|
136
|
-
id: z.string(),
|
|
137
|
-
title: z.string().optional(),
|
|
138
|
-
directory: z.string().optional(),
|
|
139
|
-
parentID: z.string().optional(),
|
|
140
|
-
time: z
|
|
141
|
-
.object({
|
|
142
|
-
created: z.number().optional(),
|
|
143
|
-
updated: z.number().optional(),
|
|
144
|
-
})
|
|
145
|
-
.loose()
|
|
146
|
-
.optional(),
|
|
147
|
-
})
|
|
148
|
-
.loose();
|
|
149
|
-
const OpenCodeMessageSchema = z
|
|
150
|
-
.object({
|
|
151
|
-
id: z.string(),
|
|
152
|
-
role: z.string().optional(),
|
|
153
|
-
time: z.object({ created: z.number().optional() }).loose().optional(),
|
|
154
|
-
})
|
|
155
|
-
.loose();
|
|
156
|
-
const OpenCodePartSchema = z
|
|
157
|
-
.object({
|
|
158
|
-
type: z.string().optional(),
|
|
159
|
-
text: z.string().optional(),
|
|
160
|
-
synthetic: z.boolean().optional(),
|
|
161
|
-
})
|
|
162
|
-
.loose();
|
|
163
|
-
// ---------- argv ----------
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { extractMemDialogue, listMemProjects, listMemSessions, MemSessionNotFoundError, readMemContext, searchMemSessions, } from "@mindfoldhq/trellis-core/mem";
|
|
164
20
|
export function parseArgv(argv) {
|
|
165
21
|
const cmd = argv[0] ?? "list";
|
|
166
22
|
const positional = [];
|
|
@@ -184,754 +40,72 @@ export function parseArgv(argv) {
|
|
|
184
40
|
positional.push(a);
|
|
185
41
|
}
|
|
186
42
|
}
|
|
187
|
-
return
|
|
43
|
+
return { cmd, positional, flags };
|
|
188
44
|
}
|
|
45
|
+
const VALID_PLATFORMS = [
|
|
46
|
+
"claude",
|
|
47
|
+
"codex",
|
|
48
|
+
"opencode",
|
|
49
|
+
"all",
|
|
50
|
+
];
|
|
51
|
+
/** Translate parsed CLI flags into a core `MemFilter`. Validation failures
|
|
52
|
+
* exit the process — core never sees raw CLI flags. */
|
|
189
53
|
export function buildFilter(flags) {
|
|
190
54
|
const platformRaw = typeof flags.platform === "string" ? flags.platform : "all";
|
|
191
|
-
|
|
192
|
-
.union([PlatformSchema, z.literal("all")])
|
|
193
|
-
.safeParse(platformRaw);
|
|
194
|
-
if (!platformParsed.success)
|
|
55
|
+
if (!VALID_PLATFORMS.includes(platformRaw))
|
|
195
56
|
die(`unknown platform: ${platformRaw}`);
|
|
57
|
+
const platform = platformRaw;
|
|
196
58
|
const sinceRaw = flags.since;
|
|
197
59
|
const since = typeof sinceRaw === "string" ? new Date(sinceRaw) : undefined;
|
|
198
60
|
if (since && Number.isNaN(+since))
|
|
199
|
-
die(`bad --since: ${sinceRaw}`);
|
|
61
|
+
die(`bad --since: ${String(sinceRaw)}`);
|
|
200
62
|
const untilRaw = flags.until;
|
|
201
63
|
const until = typeof untilRaw === "string"
|
|
202
64
|
? new Date(`${untilRaw}T23:59:59.999Z`)
|
|
203
65
|
: undefined;
|
|
204
66
|
if (until && Number.isNaN(+until))
|
|
205
|
-
die(`bad --until: ${untilRaw}`);
|
|
67
|
+
die(`bad --until: ${String(untilRaw)}`);
|
|
206
68
|
const cwd = flags.global
|
|
207
69
|
? undefined
|
|
208
70
|
: path.resolve(typeof flags.cwd === "string" ? flags.cwd : process.cwd());
|
|
209
|
-
const limit =
|
|
210
|
-
return
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
71
|
+
const limit = parseOptionalNumberFlag(flags.limit, "--limit", 50);
|
|
72
|
+
return { platform, since, until, cwd, limit };
|
|
73
|
+
}
|
|
74
|
+
function parseOptionalNumberFlag(raw, name, fallback) {
|
|
75
|
+
if (raw === undefined || raw === false)
|
|
76
|
+
return fallback;
|
|
77
|
+
if (typeof raw !== "string")
|
|
78
|
+
die(`${name} requires a number`);
|
|
79
|
+
const value = Number(raw);
|
|
80
|
+
if (!Number.isFinite(value))
|
|
81
|
+
die(`bad ${name}: ${raw}`);
|
|
82
|
+
return value;
|
|
217
83
|
}
|
|
218
84
|
function die(msg) {
|
|
219
85
|
console.error(`error: ${msg}`);
|
|
220
86
|
process.exit(2);
|
|
221
87
|
}
|
|
222
|
-
// ----------
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
return false;
|
|
232
|
-
if (f.until && t > f.until)
|
|
233
|
-
return false;
|
|
234
|
-
return true;
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Interval-overlap version of `inRange` for sessions with both start and end
|
|
238
|
-
* timestamps. A session is kept iff its lifetime `[start, end]` overlaps the
|
|
239
|
-
* query window `[f.since, f.until]`.
|
|
240
|
-
*
|
|
241
|
-
* Why this exists: long / cross-day sessions (created on day N, still updated
|
|
242
|
-
* on day N+M) were being dropped by `inRange(created, f)` when `--since` fell
|
|
243
|
-
* after `created`. Switching to interval overlap keeps sessions that were
|
|
244
|
-
* active inside the window even when they started before it.
|
|
245
|
-
*
|
|
246
|
-
* Degenerate inputs:
|
|
247
|
-
* - both undefined → pass through (no timestamp = don't filter)
|
|
248
|
-
* - one undefined → fall back to single-point semantics on the other end
|
|
249
|
-
* - unparseable iso → defer to the parsable end (or pass through if both bad)
|
|
250
|
-
*/
|
|
251
|
-
export function inRangeOverlap(start, end, f) {
|
|
252
|
-
const s = start ?? end;
|
|
253
|
-
const e = end ?? start;
|
|
254
|
-
if (!s && !e)
|
|
255
|
-
return true;
|
|
256
|
-
if (f.since && e) {
|
|
257
|
-
const eT = new Date(e);
|
|
258
|
-
if (!Number.isNaN(+eT) && eT < f.since)
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
if (f.until && s) {
|
|
262
|
-
const sT = new Date(s);
|
|
263
|
-
if (!Number.isNaN(+sT) && sT > f.until)
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
return true;
|
|
267
|
-
}
|
|
268
|
-
export function sameProject(sessionCwd, target) {
|
|
269
|
-
if (!target)
|
|
270
|
-
return true;
|
|
271
|
-
if (!sessionCwd)
|
|
272
|
-
return false;
|
|
273
|
-
const a = path.resolve(sessionCwd);
|
|
274
|
-
const b = path.resolve(target);
|
|
275
|
-
return a === b || a.startsWith(b + path.sep);
|
|
276
|
-
}
|
|
277
|
-
/** Walk JSONL line-by-line, calling `onLine` with each parsed object that
|
|
278
|
-
* matches the supplied schema. Bad JSON or schema-mismatched lines are skipped.
|
|
279
|
-
* Returning the literal "stop" from `onLine` halts iteration. */
|
|
280
|
-
function readJsonl(file, schema, onLine) {
|
|
281
|
-
let data;
|
|
282
|
-
try {
|
|
283
|
-
data = fs.readFileSync(file, "utf8");
|
|
284
|
-
}
|
|
285
|
-
catch {
|
|
88
|
+
// ---------- OpenCode reader notice ----------
|
|
89
|
+
//
|
|
90
|
+
// OpenCode 1.2+ moved to a SQLite store; the native dependency was reverted in
|
|
91
|
+
// 0.6.0-beta.4 due to install failures. Core's OpenCode adapter is a silent
|
|
92
|
+
// no-op — surfacing the degraded state is a CLI presentation concern, emitted
|
|
93
|
+
// once per process whenever the OpenCode source is in scope.
|
|
94
|
+
let opencodeWarned = false;
|
|
95
|
+
function warnOpencodeUnavailable() {
|
|
96
|
+
if (opencodeWarned)
|
|
286
97
|
return;
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
let raw;
|
|
292
|
-
try {
|
|
293
|
-
raw = JSON.parse(line);
|
|
294
|
-
}
|
|
295
|
-
catch {
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
const parsed = schema.safeParse(raw);
|
|
299
|
-
if (!parsed.success)
|
|
300
|
-
continue;
|
|
301
|
-
if (onLine(parsed.data) === "stop")
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
98
|
+
opencodeWarned = true;
|
|
99
|
+
process.stderr.write("⚠️ tl mem: OpenCode platform reader is temporarily unavailable in this build.\n" +
|
|
100
|
+
" OpenCode 1.2+ moved to SQLite; the native dependency was reverted in\n" +
|
|
101
|
+
" 0.6.0-beta.4 due to install failures. Re-enabled in a future release.\n");
|
|
304
102
|
}
|
|
305
|
-
function
|
|
306
|
-
let result;
|
|
307
|
-
readJsonl(file, schema, (obj) => {
|
|
308
|
-
result = obj;
|
|
309
|
-
return "stop";
|
|
310
|
-
});
|
|
311
|
-
return result;
|
|
312
|
-
}
|
|
313
|
-
function findInJsonl(file, schema, predicate, maxLines = 200) {
|
|
314
|
-
let count = 0;
|
|
315
|
-
let hit;
|
|
316
|
-
readJsonl(file, schema, (obj) => {
|
|
317
|
-
count++;
|
|
318
|
-
if (predicate(obj)) {
|
|
319
|
-
hit = obj;
|
|
320
|
-
return "stop";
|
|
321
|
-
}
|
|
322
|
-
if (count >= maxLines)
|
|
323
|
-
return "stop";
|
|
324
|
-
});
|
|
325
|
-
return hit;
|
|
326
|
-
}
|
|
327
|
-
function readJsonFile(file, schema) {
|
|
328
|
-
let raw;
|
|
329
|
-
try {
|
|
330
|
-
raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
331
|
-
}
|
|
332
|
-
catch {
|
|
333
|
-
return undefined;
|
|
334
|
-
}
|
|
335
|
-
const parsed = schema.safeParse(raw);
|
|
336
|
-
return parsed.success ? parsed.data : undefined;
|
|
337
|
-
}
|
|
338
|
-
// ---------- dialogue cleaning ----------
|
|
339
|
-
const INJECTION_TAGS = [
|
|
340
|
-
"system-reminder",
|
|
341
|
-
"task-status",
|
|
342
|
-
"ready",
|
|
343
|
-
"current-state",
|
|
344
|
-
"workflow",
|
|
345
|
-
"workflow-state",
|
|
346
|
-
"guidelines",
|
|
347
|
-
"instructions",
|
|
348
|
-
"command-name",
|
|
349
|
-
"command-message",
|
|
350
|
-
"command-args",
|
|
351
|
-
"local-command-stdout",
|
|
352
|
-
"local-command-stderr",
|
|
353
|
-
"permissions instructions",
|
|
354
|
-
"collaboration_mode",
|
|
355
|
-
"environment_context",
|
|
356
|
-
"auto_compact_summary",
|
|
357
|
-
"user_instructions",
|
|
358
|
-
];
|
|
359
|
-
/** True if this turn is a platform bootstrap injection (AGENTS.md, pure
|
|
360
|
-
* INSTRUCTIONS preamble, etc.) and should be dropped wholesale rather than
|
|
361
|
-
* partially cleaned. Detected after stripInjectionTags, so we look at what's
|
|
362
|
-
* left after tag-stripping. */
|
|
363
|
-
export function isBootstrapTurn(cleaned, originalLength) {
|
|
364
|
-
if (cleaned.startsWith("# AGENTS.md instructions for"))
|
|
365
|
-
return true;
|
|
366
|
-
// A turn that's mostly an INSTRUCTIONS block (Codex injects this as user role).
|
|
367
|
-
if (originalLength > 4000 && /^<INSTRUCTIONS>/i.test(cleaned))
|
|
368
|
-
return true;
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
371
|
-
export function stripInjectionTags(text) {
|
|
372
|
-
let out = text;
|
|
373
|
-
for (const tag of INJECTION_TAGS) {
|
|
374
|
-
const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
375
|
-
// Case-insensitive: Codex/Trellis injection tags appear as both <INSTRUCTIONS>
|
|
376
|
-
// and <instructions> across platforms.
|
|
377
|
-
out = out.replace(new RegExp(`<${escaped}[^>]*>[\\s\\S]*?</${escaped}>`, "gi"), "");
|
|
378
|
-
}
|
|
379
|
-
out = out.replace(/^# AGENTS\.md instructions for[\s\S]*?(?=\n\n[A-Z一-龥]|$)/m, "");
|
|
380
|
-
return out.replace(/\n{3,}/g, "\n\n").trim();
|
|
381
|
-
}
|
|
382
|
-
/** Find the paragraph-aligned chunk surrounding a hit position. A "chunk" is
|
|
383
|
-
* the contiguous text bounded by the nearest blank-line breaks (`\n\n`) on
|
|
384
|
-
* either side. If the natural paragraph exceeds `maxChars`, fall back to a
|
|
385
|
-
* centered char window — and report the truncation so callers can mark it. */
|
|
386
|
-
export function chunkAround(text, hitIdx, maxChars) {
|
|
387
|
-
const startPara = text.lastIndexOf("\n\n", hitIdx);
|
|
388
|
-
let start = startPara === -1 ? 0 : startPara + 2;
|
|
389
|
-
const endPara = text.indexOf("\n\n", hitIdx);
|
|
390
|
-
let end = endPara === -1 ? text.length : endPara;
|
|
391
|
-
let truncated = false;
|
|
392
|
-
if (end - start > maxChars) {
|
|
393
|
-
start = Math.max(0, hitIdx - Math.floor(maxChars / 2));
|
|
394
|
-
end = Math.min(text.length, hitIdx + Math.ceil(maxChars / 2));
|
|
395
|
-
truncated = true;
|
|
396
|
-
}
|
|
397
|
-
return { start, end, truncated };
|
|
398
|
-
}
|
|
399
|
-
/** Multi-token AND grep over cleaned dialogue. Whitespace-split tokens; a
|
|
400
|
-
* turn matches if every token (case-insensitive) appears anywhere in it.
|
|
401
|
-
* `count` is the total occurrence count across all tokens within matching
|
|
402
|
-
* turns. Excerpts are paragraph-aligned chunks (drawer-style): for each
|
|
403
|
-
* matching turn we collect chunks around every hit position, dedupe by
|
|
404
|
-
* chunk start so adjacent hits inside the same paragraph collapse to one
|
|
405
|
-
* chunk. User-role chunks are listed first (the user's own words anchor
|
|
406
|
-
* topic intent more reliably than AI elaboration). */
|
|
407
|
-
export function searchInDialogue(turns, kw, maxExcerpts = 3, chunkChars = 400) {
|
|
408
|
-
const tokens = kw.toLowerCase().split(/\s+/).filter(Boolean);
|
|
409
|
-
const empty = SearchHitSchema.parse({
|
|
410
|
-
count: 0,
|
|
411
|
-
user_count: 0,
|
|
412
|
-
asst_count: 0,
|
|
413
|
-
total_turns: turns.length,
|
|
414
|
-
excerpts: [],
|
|
415
|
-
});
|
|
416
|
-
if (tokens.length === 0)
|
|
417
|
-
return empty;
|
|
418
|
-
let userCount = 0;
|
|
419
|
-
let asstCount = 0;
|
|
420
|
-
const userExcerpts = [];
|
|
421
|
-
const asstExcerpts = [];
|
|
422
|
-
for (const t of turns) {
|
|
423
|
-
const hay = t.text.toLowerCase();
|
|
424
|
-
if (!tokens.every((tok) => hay.includes(tok)))
|
|
425
|
-
continue;
|
|
426
|
-
// Collect every hit position with the token that produced it (for both
|
|
427
|
-
// counting and rarity-aware chunk anchor selection).
|
|
428
|
-
const hitPositions = [];
|
|
429
|
-
const tokenFreq = new Map();
|
|
430
|
-
let turnHits = 0;
|
|
431
|
-
for (const tok of tokens) {
|
|
432
|
-
let from = 0;
|
|
433
|
-
let n = 0;
|
|
434
|
-
while (true) {
|
|
435
|
-
const idx = hay.indexOf(tok, from);
|
|
436
|
-
if (idx === -1)
|
|
437
|
-
break;
|
|
438
|
-
n++;
|
|
439
|
-
turnHits++;
|
|
440
|
-
hitPositions.push({ idx, tok });
|
|
441
|
-
from = idx + tok.length;
|
|
442
|
-
}
|
|
443
|
-
tokenFreq.set(tok, n);
|
|
444
|
-
}
|
|
445
|
-
if (t.role === "user")
|
|
446
|
-
userCount += turnHits;
|
|
447
|
-
else
|
|
448
|
-
asstCount += turnHits;
|
|
449
|
-
hitPositions.sort((a, b) => a.idx - b.idx);
|
|
450
|
-
const candidates = [];
|
|
451
|
-
const seenStarts = new Set();
|
|
452
|
-
for (const { idx, tok } of hitPositions) {
|
|
453
|
-
const { start, end, truncated } = chunkAround(t.text, idx, chunkChars);
|
|
454
|
-
if (seenStarts.has(start))
|
|
455
|
-
continue;
|
|
456
|
-
seenStarts.add(start);
|
|
457
|
-
const slice = hay.slice(start, end);
|
|
458
|
-
const coverage = tokens.filter((tk) => slice.includes(tk)).length;
|
|
459
|
-
const rarity = 1 / (tokenFreq.get(tok) ?? 1);
|
|
460
|
-
candidates.push({ start, end, truncated, coverage, rarity });
|
|
461
|
-
}
|
|
462
|
-
candidates.sort((a, b) => {
|
|
463
|
-
if (b.coverage !== a.coverage)
|
|
464
|
-
return b.coverage - a.coverage;
|
|
465
|
-
if (b.rarity !== a.rarity)
|
|
466
|
-
return b.rarity - a.rarity;
|
|
467
|
-
return a.start - b.start;
|
|
468
|
-
});
|
|
469
|
-
for (const c of candidates) {
|
|
470
|
-
let snippet = t.text.slice(c.start, c.end).trim();
|
|
471
|
-
if (c.truncated) {
|
|
472
|
-
if (c.start > 0)
|
|
473
|
-
snippet = "…" + snippet;
|
|
474
|
-
if (c.end < t.text.length)
|
|
475
|
-
snippet += "…";
|
|
476
|
-
}
|
|
477
|
-
(t.role === "user" ? userExcerpts : asstExcerpts).push({
|
|
478
|
-
role: t.role,
|
|
479
|
-
snippet,
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
const excerpts = [...userExcerpts, ...asstExcerpts].slice(0, maxExcerpts);
|
|
484
|
-
return SearchHitSchema.parse({
|
|
485
|
-
count: userCount + asstCount,
|
|
486
|
-
user_count: userCount,
|
|
487
|
-
asst_count: asstCount,
|
|
488
|
-
total_turns: turns.length,
|
|
489
|
-
excerpts,
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
// ---------- claude adapter ----------
|
|
493
|
-
const CLAUDE_PROJECTS = path.join(HOME, ".claude", "projects");
|
|
494
|
-
function claudeProjectDirFromCwd(cwd) {
|
|
495
|
-
// Claude sanitizes path: every '/' and '_' becomes '-'.
|
|
496
|
-
return path.join(CLAUDE_PROJECTS, cwd.replace(/[/_]/g, "-"));
|
|
497
|
-
}
|
|
498
|
-
export function claudeListSessions(f) {
|
|
499
|
-
if (!fs.existsSync(CLAUDE_PROJECTS))
|
|
500
|
-
return [];
|
|
501
|
-
const out = [];
|
|
502
|
-
const projectDirs = f.cwd
|
|
503
|
-
? [claudeProjectDirFromCwd(f.cwd)].filter((d) => fs.existsSync(d))
|
|
504
|
-
: fs.readdirSync(CLAUDE_PROJECTS).map((d) => path.join(CLAUDE_PROJECTS, d));
|
|
505
|
-
for (const dir of projectDirs) {
|
|
506
|
-
let entries;
|
|
507
|
-
try {
|
|
508
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
509
|
-
}
|
|
510
|
-
catch {
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
const indexFile = path.join(dir, "sessions-index.json");
|
|
514
|
-
const index = readJsonFile(indexFile, ClaudeIndexSchema);
|
|
515
|
-
const indexById = new Map();
|
|
516
|
-
for (const e of index?.entries ?? [])
|
|
517
|
-
indexById.set(e.id, e);
|
|
518
|
-
for (const e of entries) {
|
|
519
|
-
if (!e.isFile() || !e.name.endsWith(".jsonl"))
|
|
520
|
-
continue;
|
|
521
|
-
const filePath = path.join(dir, e.name);
|
|
522
|
-
const id = e.name.replace(/\.jsonl$/, "");
|
|
523
|
-
const idx = indexById.get(id);
|
|
524
|
-
let cwd = idx?.cwd;
|
|
525
|
-
let created = idx?.created;
|
|
526
|
-
const title = idx?.title;
|
|
527
|
-
if (!cwd || !created) {
|
|
528
|
-
const evt = findInJsonl(filePath, ClaudeEventSchema, (o) => typeof o.cwd === "string", 100);
|
|
529
|
-
cwd = cwd ?? evt?.cwd;
|
|
530
|
-
created =
|
|
531
|
-
created ??
|
|
532
|
-
evt?.timestamp ??
|
|
533
|
-
readJsonlFirst(filePath, ClaudeEventSchema)?.timestamp;
|
|
534
|
-
}
|
|
535
|
-
const stat = fs.statSync(filePath);
|
|
536
|
-
const updated = stat.mtime.toISOString();
|
|
537
|
-
// Interval overlap: keep sessions whose lifetime [created, updated]
|
|
538
|
-
// intersects the query window. Cross-day sessions (created before
|
|
539
|
-
// --since but still active inside it) must survive — see PRD
|
|
540
|
-
// 05-08-mem-since-cross-day-filter.
|
|
541
|
-
if (!inRangeOverlap(created, updated, f))
|
|
542
|
-
continue;
|
|
543
|
-
if (f.cwd && cwd && !sameProject(cwd, f.cwd))
|
|
544
|
-
continue;
|
|
545
|
-
out.push(SessionInfoSchema.parse({
|
|
546
|
-
platform: "claude",
|
|
547
|
-
id,
|
|
548
|
-
title,
|
|
549
|
-
cwd,
|
|
550
|
-
created,
|
|
551
|
-
updated,
|
|
552
|
-
filePath,
|
|
553
|
-
}));
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
return out;
|
|
557
|
-
}
|
|
558
|
-
export function claudeExtractDialogue(s) {
|
|
559
|
-
// Mirrors session-insight/extract-session.py:
|
|
560
|
-
// - user: type=="user" + role=="user" + content is string (list = tool_result)
|
|
561
|
-
// - assistant: type=="assistant" + role=="assistant", keep only `text` blocks
|
|
562
|
-
// - thinking and tool_use blocks dropped entirely
|
|
563
|
-
// - injection tags stripped
|
|
564
|
-
// Compaction: when we hit a `user` event with isCompactSummary=true, drop all
|
|
565
|
-
// pre-compact turns and replace them with a synthetic [compact summary] turn —
|
|
566
|
-
// the pre-compact content is now redundant with the summary.
|
|
567
|
-
let turns = [];
|
|
568
|
-
readJsonl(s.filePath, ClaudeEventSchema, (obj) => {
|
|
569
|
-
const t = obj.type;
|
|
570
|
-
const msg = obj.message;
|
|
571
|
-
if (!msg)
|
|
572
|
-
return;
|
|
573
|
-
const content = msg.content;
|
|
574
|
-
if (t === "user" && obj.isCompactSummary === true) {
|
|
575
|
-
let summary = "";
|
|
576
|
-
if (typeof content === "string") {
|
|
577
|
-
summary = stripInjectionTags(content);
|
|
578
|
-
}
|
|
579
|
-
else if (Array.isArray(content)) {
|
|
580
|
-
const parts = [];
|
|
581
|
-
for (const block of content) {
|
|
582
|
-
if (block.type === "text" && typeof block.text === "string") {
|
|
583
|
-
const cleaned = stripInjectionTags(block.text);
|
|
584
|
-
if (cleaned)
|
|
585
|
-
parts.push(cleaned);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
summary = parts.join("\n\n");
|
|
589
|
-
}
|
|
590
|
-
turns = summary
|
|
591
|
-
? [{ role: "user", text: `[compact summary]\n${summary}` }]
|
|
592
|
-
: [];
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
if (t === "user" && msg.role === "user") {
|
|
596
|
-
if (typeof content === "string") {
|
|
597
|
-
const text = stripInjectionTags(content);
|
|
598
|
-
if (text && !isBootstrapTurn(text, content.length)) {
|
|
599
|
-
turns.push({ role: "user", text });
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
else if (t === "assistant" &&
|
|
604
|
-
msg.role === "assistant" &&
|
|
605
|
-
Array.isArray(content)) {
|
|
606
|
-
const parts = [];
|
|
607
|
-
for (const block of content) {
|
|
608
|
-
if (block.type === "text" && typeof block.text === "string") {
|
|
609
|
-
const cleaned = stripInjectionTags(block.text);
|
|
610
|
-
if (cleaned)
|
|
611
|
-
parts.push(cleaned);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
if (parts.length)
|
|
615
|
-
turns.push({ role: "assistant", text: parts.join("\n\n") });
|
|
616
|
-
}
|
|
617
|
-
});
|
|
618
|
-
return turns;
|
|
619
|
-
}
|
|
620
|
-
export function claudeSearch(s, kw) {
|
|
621
|
-
return searchInDialogue(claudeExtractDialogue(s), kw);
|
|
622
|
-
}
|
|
623
|
-
// ---------- codex adapter ----------
|
|
624
|
-
const CODEX_SESSIONS = path.join(HOME, ".codex", "sessions");
|
|
625
|
-
function* walkDir(root) {
|
|
626
|
-
if (!fs.existsSync(root))
|
|
627
|
-
return;
|
|
628
|
-
const stack = [root];
|
|
629
|
-
while (stack.length) {
|
|
630
|
-
const cur = stack.pop();
|
|
631
|
-
if (cur === undefined)
|
|
632
|
-
break;
|
|
633
|
-
let entries;
|
|
634
|
-
try {
|
|
635
|
-
entries = fs.readdirSync(cur, { withFileTypes: true });
|
|
636
|
-
}
|
|
637
|
-
catch {
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
for (const e of entries) {
|
|
641
|
-
const p = path.join(cur, e.name);
|
|
642
|
-
if (e.isDirectory())
|
|
643
|
-
stack.push(p);
|
|
644
|
-
else if (e.isFile())
|
|
645
|
-
yield p;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
export function codexListSessions(f) {
|
|
650
|
-
if (!fs.existsSync(CODEX_SESSIONS))
|
|
651
|
-
return [];
|
|
652
|
-
const out = [];
|
|
653
|
-
for (const file of walkDir(CODEX_SESSIONS)) {
|
|
654
|
-
if (!file.endsWith(".jsonl"))
|
|
655
|
-
continue;
|
|
656
|
-
const base = path.basename(file, ".jsonl");
|
|
657
|
-
const m = base.match(/^rollout-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})-(.+)$/);
|
|
658
|
-
const tsFromName = m?.[1]
|
|
659
|
-
? new Date(m[1].replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3") + "Z").toISOString()
|
|
660
|
-
: undefined;
|
|
661
|
-
// Note: we previously short-circuited on `!inRange(tsFromName, f)` here,
|
|
662
|
-
// but the filename ts is the session's creation time — a cross-day session
|
|
663
|
-
// that started before --since but was active inside it would be dropped.
|
|
664
|
-
// Filter at the same place as claude/opencode using interval overlap.
|
|
665
|
-
const first = readJsonlFirst(file, CodexEventSchema);
|
|
666
|
-
const meta = first?.payload;
|
|
667
|
-
const id = meta?.id ?? m?.[2] ?? base;
|
|
668
|
-
const cwd = meta?.cwd;
|
|
669
|
-
const created = first?.timestamp ?? tsFromName ?? "";
|
|
670
|
-
if (f.cwd && !sameProject(cwd, f.cwd))
|
|
671
|
-
continue;
|
|
672
|
-
const updated = fs.statSync(file).mtime.toISOString();
|
|
673
|
-
if (!inRangeOverlap(created, updated, f))
|
|
674
|
-
continue;
|
|
675
|
-
out.push(SessionInfoSchema.parse({
|
|
676
|
-
platform: "codex",
|
|
677
|
-
id,
|
|
678
|
-
cwd,
|
|
679
|
-
created,
|
|
680
|
-
updated,
|
|
681
|
-
filePath: file,
|
|
682
|
-
}));
|
|
683
|
-
}
|
|
684
|
-
return out;
|
|
685
|
-
}
|
|
686
|
-
export function codexExtractDialogue(s) {
|
|
687
|
-
// Codex events: payload.type=="message" with role in {user, assistant, developer, system}.
|
|
688
|
-
// Keep user/assistant only. Each content part is {type: "input_text"|"output_text", text}.
|
|
689
|
-
// Codex inlines a lot of system prompt as the first user message (AGENTS.md, permission
|
|
690
|
-
// blocks, etc.) — stripInjectionTags removes the bulk; turns that are pure boilerplate
|
|
691
|
-
// collapse to empty after strip and get dropped here.
|
|
692
|
-
// Compaction: a top-level event with type=="compacted" carries a payload.replacement_history
|
|
693
|
-
// array — the new authoritative history replacing everything before. We reset turns and
|
|
694
|
-
// re-seed from replacement_history.
|
|
695
|
-
let turns = [];
|
|
696
|
-
const buildTurnFromMessage = (role, parts) => {
|
|
697
|
-
const collected = [];
|
|
698
|
-
let totalRaw = 0;
|
|
699
|
-
for (const c of parts ?? []) {
|
|
700
|
-
const txt = c.text;
|
|
701
|
-
if (typeof txt !== "string")
|
|
702
|
-
continue;
|
|
703
|
-
if (c.type !== "input_text" && c.type !== "output_text")
|
|
704
|
-
continue;
|
|
705
|
-
totalRaw += txt.length;
|
|
706
|
-
const cleaned = stripInjectionTags(txt);
|
|
707
|
-
if (cleaned)
|
|
708
|
-
collected.push(cleaned);
|
|
709
|
-
}
|
|
710
|
-
if (!collected.length)
|
|
711
|
-
return null;
|
|
712
|
-
const merged = collected.join("\n\n");
|
|
713
|
-
if (isBootstrapTurn(merged, totalRaw))
|
|
714
|
-
return null;
|
|
715
|
-
return { role, text: merged };
|
|
716
|
-
};
|
|
717
|
-
readJsonl(s.filePath, CodexEventSchema, (obj) => {
|
|
718
|
-
if (obj.type === "compacted") {
|
|
719
|
-
const rh = obj.payload?.replacement_history;
|
|
720
|
-
turns = [];
|
|
721
|
-
if (!Array.isArray(rh))
|
|
722
|
-
return;
|
|
723
|
-
for (const item of rh) {
|
|
724
|
-
if (item.type !== "message")
|
|
725
|
-
continue;
|
|
726
|
-
const r = DialogueRoleSchema.safeParse(item.role);
|
|
727
|
-
if (!r.success)
|
|
728
|
-
continue;
|
|
729
|
-
const turn = buildTurnFromMessage(r.data, item.content);
|
|
730
|
-
if (turn)
|
|
731
|
-
turns.push({ role: turn.role, text: `[compact]\n${turn.text}` });
|
|
732
|
-
}
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
const p = obj.payload;
|
|
736
|
-
if (p?.type !== "message")
|
|
737
|
-
return;
|
|
738
|
-
const roleParsed = DialogueRoleSchema.safeParse(p.role);
|
|
739
|
-
if (!roleParsed.success)
|
|
740
|
-
return;
|
|
741
|
-
const turn = buildTurnFromMessage(roleParsed.data, p.content);
|
|
742
|
-
if (turn)
|
|
743
|
-
turns.push(turn);
|
|
744
|
-
});
|
|
745
|
-
return turns;
|
|
746
|
-
}
|
|
747
|
-
export function codexSearch(s, kw) {
|
|
748
|
-
return searchInDialogue(codexExtractDialogue(s), kw);
|
|
749
|
-
}
|
|
750
|
-
// ---------- opencode adapter ----------
|
|
751
|
-
const OC_ROOT = path.join(HOME, ".local", "share", "opencode", "storage");
|
|
752
|
-
const OC_SESSION_DIR = path.join(OC_ROOT, "session");
|
|
753
|
-
const OC_MESSAGE_DIR = path.join(OC_ROOT, "message");
|
|
754
|
-
const OC_PART_DIR = path.join(OC_ROOT, "part");
|
|
755
|
-
export function opencodeListSessions(f) {
|
|
756
|
-
if (!fs.existsSync(OC_SESSION_DIR))
|
|
757
|
-
return [];
|
|
758
|
-
const out = [];
|
|
759
|
-
for (const file of walkDir(OC_SESSION_DIR)) {
|
|
760
|
-
if (!file.endsWith(".json"))
|
|
761
|
-
continue;
|
|
762
|
-
const info = readJsonFile(file, OpenCodeSessionSchema);
|
|
763
|
-
if (!info)
|
|
764
|
-
continue;
|
|
765
|
-
const created = info.time?.created !== undefined
|
|
766
|
-
? new Date(info.time.created).toISOString()
|
|
767
|
-
: undefined;
|
|
768
|
-
const updated = info.time?.updated !== undefined
|
|
769
|
-
? new Date(info.time.updated).toISOString()
|
|
770
|
-
: undefined;
|
|
771
|
-
const cwd = info.directory;
|
|
772
|
-
if (f.cwd && !sameProject(cwd, f.cwd))
|
|
773
|
-
continue;
|
|
774
|
-
if (!inRangeOverlap(created, updated, f))
|
|
775
|
-
continue;
|
|
776
|
-
out.push(SessionInfoSchema.parse({
|
|
777
|
-
platform: "opencode",
|
|
778
|
-
id: info.id,
|
|
779
|
-
title: info.title,
|
|
780
|
-
cwd,
|
|
781
|
-
created,
|
|
782
|
-
updated,
|
|
783
|
-
filePath: file,
|
|
784
|
-
messageDir: path.join(OC_MESSAGE_DIR, info.id),
|
|
785
|
-
parent_id: info.parentID,
|
|
786
|
-
}));
|
|
787
|
-
}
|
|
788
|
-
return out;
|
|
789
|
-
}
|
|
790
|
-
function opencodeListMessageFiles(messageDir) {
|
|
791
|
-
try {
|
|
792
|
-
return fs.readdirSync(messageDir).filter((n) => n.endsWith(".json"));
|
|
793
|
-
}
|
|
794
|
-
catch {
|
|
795
|
-
return [];
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
export function opencodeExtractDialogue(s) {
|
|
799
|
-
// OpenCode: messages live at message/<sid>/msg_*.json, part bodies at part/<msgId>/prt_*.json.
|
|
800
|
-
// Keep parts with type=="text" && synthetic !== true; group by message; dialogue role
|
|
801
|
-
// comes from the message file's `role` field. Synthetic parts are platform-injected
|
|
802
|
-
// preamble (mode prompts, agent boilerplate) and are dropped as noise.
|
|
803
|
-
const turns = [];
|
|
804
|
-
if (!s.messageDir || !fs.existsSync(s.messageDir))
|
|
805
|
-
return turns;
|
|
806
|
-
const ordered = [];
|
|
807
|
-
for (const mf of opencodeListMessageFiles(s.messageDir)) {
|
|
808
|
-
const msg = readJsonFile(path.join(s.messageDir, mf), OpenCodeMessageSchema);
|
|
809
|
-
if (msg)
|
|
810
|
-
ordered.push({ msg, created: msg.time?.created ?? 0 });
|
|
811
|
-
}
|
|
812
|
-
ordered.sort((a, b) => a.created - b.created);
|
|
813
|
-
for (const { msg } of ordered) {
|
|
814
|
-
const roleParsed = DialogueRoleSchema.safeParse(msg.role);
|
|
815
|
-
if (!roleParsed.success)
|
|
816
|
-
continue;
|
|
817
|
-
const partDir = path.join(OC_PART_DIR, msg.id);
|
|
818
|
-
if (!fs.existsSync(partDir))
|
|
819
|
-
continue;
|
|
820
|
-
let parts;
|
|
821
|
-
try {
|
|
822
|
-
parts = fs.readdirSync(partDir).filter((n) => n.endsWith(".json"));
|
|
823
|
-
}
|
|
824
|
-
catch {
|
|
825
|
-
continue;
|
|
826
|
-
}
|
|
827
|
-
const collected = [];
|
|
828
|
-
let totalRaw = 0;
|
|
829
|
-
for (const pf of parts) {
|
|
830
|
-
const part = readJsonFile(path.join(partDir, pf), OpenCodePartSchema);
|
|
831
|
-
if (!part)
|
|
832
|
-
continue;
|
|
833
|
-
if (part.type !== "text" || part.synthetic)
|
|
834
|
-
continue;
|
|
835
|
-
if (typeof part.text !== "string")
|
|
836
|
-
continue;
|
|
837
|
-
totalRaw += part.text.length;
|
|
838
|
-
const cleaned = stripInjectionTags(part.text);
|
|
839
|
-
if (cleaned)
|
|
840
|
-
collected.push(cleaned);
|
|
841
|
-
}
|
|
842
|
-
if (!collected.length)
|
|
843
|
-
continue;
|
|
844
|
-
const merged = collected.join("\n\n");
|
|
845
|
-
if (isBootstrapTurn(merged, totalRaw))
|
|
846
|
-
continue;
|
|
847
|
-
turns.push({ role: roleParsed.data, text: merged });
|
|
848
|
-
}
|
|
849
|
-
return turns;
|
|
850
|
-
}
|
|
851
|
-
function opencodeSearch(s, kw) {
|
|
852
|
-
const turns = opencodeExtractDialogue(s);
|
|
853
|
-
if (s.title)
|
|
854
|
-
turns.unshift({ role: "user", text: s.title });
|
|
855
|
-
return searchInDialogue(turns, kw);
|
|
856
|
-
}
|
|
857
|
-
// ---------- dispatch ----------
|
|
858
|
-
function listAll(f) {
|
|
859
|
-
const all = [];
|
|
860
|
-
if (f.platform === "all" || f.platform === "claude")
|
|
861
|
-
all.push(...claudeListSessions(f));
|
|
862
|
-
if (f.platform === "all" || f.platform === "codex")
|
|
863
|
-
all.push(...codexListSessions(f));
|
|
103
|
+
function maybeWarnOpencode(f) {
|
|
864
104
|
if (f.platform === "all" || f.platform === "opencode")
|
|
865
|
-
|
|
866
|
-
all.sort((a, b) => (b.updated ?? b.created ?? "").localeCompare(a.updated ?? a.created ?? ""));
|
|
867
|
-
return all.slice(0, f.limit);
|
|
868
|
-
}
|
|
869
|
-
function extractDialogue(s) {
|
|
870
|
-
switch (s.platform) {
|
|
871
|
-
case "claude":
|
|
872
|
-
return claudeExtractDialogue(s);
|
|
873
|
-
case "codex":
|
|
874
|
-
return codexExtractDialogue(s);
|
|
875
|
-
case "opencode":
|
|
876
|
-
return opencodeExtractDialogue(s);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
function searchSession(s, kw) {
|
|
880
|
-
switch (s.platform) {
|
|
881
|
-
case "claude":
|
|
882
|
-
return claudeSearch(s, kw);
|
|
883
|
-
case "codex":
|
|
884
|
-
return codexSearch(s, kw);
|
|
885
|
-
case "opencode":
|
|
886
|
-
return opencodeSearch(s, kw);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
/** Build parent → descendants index for OpenCode (transitively flattened).
|
|
890
|
-
* Other platforms have no native parent_id so they pass through unchanged. */
|
|
891
|
-
function buildChildIndex(sessions) {
|
|
892
|
-
const directChildren = new Map();
|
|
893
|
-
for (const s of sessions) {
|
|
894
|
-
if (!s.parent_id)
|
|
895
|
-
continue;
|
|
896
|
-
const list = directChildren.get(s.parent_id) ?? [];
|
|
897
|
-
list.push(s);
|
|
898
|
-
directChildren.set(s.parent_id, list);
|
|
899
|
-
}
|
|
900
|
-
// Transitive flatten: each parent maps to *all* descendants.
|
|
901
|
-
const out = new Map();
|
|
902
|
-
for (const [pid] of directChildren) {
|
|
903
|
-
const stack = [...(directChildren.get(pid) ?? [])];
|
|
904
|
-
const flat = [];
|
|
905
|
-
while (stack.length) {
|
|
906
|
-
const cur = stack.pop();
|
|
907
|
-
if (cur === undefined)
|
|
908
|
-
break;
|
|
909
|
-
flat.push(cur);
|
|
910
|
-
for (const c of directChildren.get(cur.id) ?? [])
|
|
911
|
-
stack.push(c);
|
|
912
|
-
}
|
|
913
|
-
out.set(pid, flat);
|
|
914
|
-
}
|
|
915
|
-
return out;
|
|
916
|
-
}
|
|
917
|
-
function searchSessionWithChildren(s, kw, childIndex) {
|
|
918
|
-
const children = childIndex.get(s.id) ?? [];
|
|
919
|
-
if (children.length === 0)
|
|
920
|
-
return searchSession(s, kw);
|
|
921
|
-
// Concatenate parent + descendants' cleaned dialogue, then run a single
|
|
922
|
-
// search over the merged turn list. This way scores reflect total topic
|
|
923
|
-
// density across the sub-agent tree.
|
|
924
|
-
const merged = [...extractDialogue(s)];
|
|
925
|
-
for (const c of children)
|
|
926
|
-
merged.push(...extractDialogue(c));
|
|
927
|
-
return searchInDialogue(merged, kw);
|
|
928
|
-
}
|
|
929
|
-
function findSessionById(id, f) {
|
|
930
|
-
const wide = { ...f, cwd: undefined, limit: 1_000_000 };
|
|
931
|
-
const all = listAll(wide);
|
|
932
|
-
return all.find((s) => s.id === id) ?? all.find((s) => s.id.startsWith(id));
|
|
105
|
+
warnOpencodeUnavailable();
|
|
933
106
|
}
|
|
934
107
|
// ---------- formatting ----------
|
|
108
|
+
const HOME = os.homedir();
|
|
935
109
|
export function shortDate(iso) {
|
|
936
110
|
if (!iso)
|
|
937
111
|
return " ";
|
|
@@ -960,7 +134,8 @@ function printSessions(rows) {
|
|
|
960
134
|
// ---------- commands ----------
|
|
961
135
|
function cmdList(argv) {
|
|
962
136
|
const f = buildFilter(argv.flags);
|
|
963
|
-
|
|
137
|
+
maybeWarnOpencode(f);
|
|
138
|
+
const rows = listMemSessions({ filter: f });
|
|
964
139
|
if (argv.flags.json) {
|
|
965
140
|
console.log(JSON.stringify(rows, null, 2));
|
|
966
141
|
return;
|
|
@@ -976,52 +151,24 @@ function cmdSearch(argv) {
|
|
|
976
151
|
if (!kw)
|
|
977
152
|
die("usage: search <keyword>");
|
|
978
153
|
const f = buildFilter(argv.flags);
|
|
979
|
-
|
|
980
|
-
const candidates = listAll(wide);
|
|
154
|
+
maybeWarnOpencode(f);
|
|
981
155
|
const includeChildren = argv.flags["include-children"] === true;
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const childIndex = includeChildren ? buildChildIndex(candidates) : new Map();
|
|
987
|
-
const candidateIds = new Set(candidates.map((s) => s.id));
|
|
988
|
-
const isAbsorbedChild = (s) => includeChildren &&
|
|
989
|
-
s.parent_id !== undefined &&
|
|
990
|
-
candidateIds.has(s.parent_id);
|
|
991
|
-
const matches = [];
|
|
992
|
-
for (const s of candidates) {
|
|
993
|
-
if (isAbsorbedChild(s))
|
|
994
|
-
continue;
|
|
995
|
-
const hit = includeChildren
|
|
996
|
-
? searchSessionWithChildren(s, kw, childIndex)
|
|
997
|
-
: searchSession(s, kw);
|
|
998
|
-
if (hit.count === 0)
|
|
999
|
-
continue;
|
|
1000
|
-
matches.push({ s, hit, descendants: childIndex.get(s.id)?.length ?? 0 });
|
|
1001
|
-
}
|
|
1002
|
-
// Rank by weighted-density relevance score: user hits matter ×3, normalized
|
|
1003
|
-
// by total dialogue length so a tight 18-hit short session beats a sprawling
|
|
1004
|
-
// 58-hit long one. Tie-break on raw count, then recency.
|
|
1005
|
-
matches.sort((a, b) => {
|
|
1006
|
-
const sa = relevanceScore(a.hit);
|
|
1007
|
-
const sb = relevanceScore(b.hit);
|
|
1008
|
-
if (sb !== sa)
|
|
1009
|
-
return sb - sa;
|
|
1010
|
-
if (b.hit.count !== a.hit.count)
|
|
1011
|
-
return b.hit.count - a.hit.count;
|
|
1012
|
-
return (b.s.updated ?? b.s.created ?? "").localeCompare(a.s.updated ?? a.s.created ?? "");
|
|
156
|
+
const result = searchMemSessions({
|
|
157
|
+
keyword: kw,
|
|
158
|
+
filter: f,
|
|
159
|
+
includeChildren,
|
|
1013
160
|
});
|
|
1014
|
-
const top = matches
|
|
161
|
+
const top = result.matches;
|
|
1015
162
|
if (argv.flags.json) {
|
|
1016
|
-
console.log(JSON.stringify(top.map((
|
|
1017
|
-
session:
|
|
1018
|
-
score: Number(
|
|
1019
|
-
hit_count: hit.count,
|
|
1020
|
-
user_count: hit.
|
|
1021
|
-
asst_count: hit.
|
|
1022
|
-
total_turns: hit.
|
|
1023
|
-
descendants_merged: includeChildren ?
|
|
1024
|
-
excerpts: hit.excerpts,
|
|
163
|
+
console.log(JSON.stringify(top.map((m) => ({
|
|
164
|
+
session: m.session,
|
|
165
|
+
score: Number(m.score.toFixed(4)),
|
|
166
|
+
hit_count: m.hit.count,
|
|
167
|
+
user_count: m.hit.userCount,
|
|
168
|
+
asst_count: m.hit.asstCount,
|
|
169
|
+
total_turns: m.hit.totalTurns,
|
|
170
|
+
descendants_merged: includeChildren ? m.descendantsMerged : 0,
|
|
171
|
+
excerpts: m.hit.excerpts,
|
|
1025
172
|
})), null, 2));
|
|
1026
173
|
return;
|
|
1027
174
|
}
|
|
@@ -1031,49 +178,30 @@ function cmdSearch(argv) {
|
|
|
1031
178
|
console.log("(no matches)");
|
|
1032
179
|
return;
|
|
1033
180
|
}
|
|
1034
|
-
for (const
|
|
181
|
+
for (const m of top) {
|
|
182
|
+
const s = m.session;
|
|
1035
183
|
const idShort = s.id.slice(0, 12);
|
|
1036
|
-
const score =
|
|
1037
|
-
const childTag = includeChildren &&
|
|
184
|
+
const score = m.score.toFixed(3);
|
|
185
|
+
const childTag = includeChildren && m.descendantsMerged > 0
|
|
186
|
+
? ` +${m.descendantsMerged} child`
|
|
187
|
+
: "";
|
|
1038
188
|
console.log(`\n[${s.platform.padEnd(8)}] ${shortDate(s.updated ?? s.created)} ${idShort} ${shortPath(s.cwd)}` +
|
|
1039
|
-
` score=${score} hits=${hit.count} (u=${hit.
|
|
189
|
+
` score=${score} hits=${m.hit.count} (u=${m.hit.userCount},a=${m.hit.asstCount}) turns=${m.hit.totalTurns}${childTag}` +
|
|
1040
190
|
(s.title ? ` — ${s.title}` : ""));
|
|
1041
|
-
for (const ex of hit.excerpts) {
|
|
191
|
+
for (const ex of m.hit.excerpts) {
|
|
1042
192
|
console.log(` [${ex.role}] ${ex.snippet}`);
|
|
1043
193
|
}
|
|
1044
194
|
}
|
|
1045
|
-
console.log(`\n${top.length} session(s)${
|
|
195
|
+
console.log(`\n${top.length} session(s)${result.totalMatches > top.length ? ` (of ${result.totalMatches})` : ""}`);
|
|
1046
196
|
}
|
|
1047
197
|
function cmdProjects(argv) {
|
|
1048
|
-
//
|
|
1049
|
-
// session counts.
|
|
1050
|
-
//
|
|
1051
|
-
// a follow-up `search`.
|
|
198
|
+
// Distinct cwds across all platforms with last-active timestamp + per-platform
|
|
199
|
+
// session counts. AI calls this first to learn which project paths have
|
|
200
|
+
// recent activity, then picks one for `--cwd` in a follow-up `search`.
|
|
1052
201
|
const f = buildFilter({ ...argv.flags, global: true });
|
|
1053
|
-
|
|
1054
|
-
const
|
|
1055
|
-
const
|
|
1056
|
-
for (const s of all) {
|
|
1057
|
-
if (!s.cwd)
|
|
1058
|
-
continue;
|
|
1059
|
-
const ts = s.updated ?? s.created ?? "";
|
|
1060
|
-
let agg = byCwd.get(s.cwd);
|
|
1061
|
-
if (!agg) {
|
|
1062
|
-
agg = {
|
|
1063
|
-
cwd: s.cwd,
|
|
1064
|
-
last_active: ts,
|
|
1065
|
-
sessions: 0,
|
|
1066
|
-
by_platform: { claude: 0, codex: 0, opencode: 0 },
|
|
1067
|
-
};
|
|
1068
|
-
byCwd.set(s.cwd, agg);
|
|
1069
|
-
}
|
|
1070
|
-
agg.sessions++;
|
|
1071
|
-
agg.by_platform[s.platform]++;
|
|
1072
|
-
if (ts > agg.last_active)
|
|
1073
|
-
agg.last_active = ts;
|
|
1074
|
-
}
|
|
1075
|
-
const rows = [...byCwd.values()].sort((a, b) => b.last_active.localeCompare(a.last_active));
|
|
1076
|
-
const limit = typeof argv.flags.limit === "string" ? Number(argv.flags.limit) : 30;
|
|
202
|
+
maybeWarnOpencode(f);
|
|
203
|
+
const rows = listMemProjects({ filter: f });
|
|
204
|
+
const limit = parseOptionalNumberFlag(argv.flags.limit, "--limit", 30);
|
|
1077
205
|
const top = rows.slice(0, limit);
|
|
1078
206
|
if (argv.flags.json) {
|
|
1079
207
|
console.log(JSON.stringify(top, null, 2));
|
|
@@ -1096,159 +224,117 @@ function cmdProjects(argv) {
|
|
|
1096
224
|
console.log(`\n${top.length} project(s)${rows.length > top.length ? ` (of ${rows.length})` : ""}`);
|
|
1097
225
|
}
|
|
1098
226
|
function cmdContext(argv) {
|
|
1099
|
-
// Drill-down step 2 in the search workflow:
|
|
1100
|
-
//
|
|
1101
|
-
//
|
|
1102
|
-
// turns of context on either side, token-budgeted for AI consumption
|
|
1103
|
-
//
|
|
1104
|
-
// Without --grep: returns the first N turns (lets AI inspect session opening).
|
|
1105
|
-
// With --grep: ranks turns by (user-role first, then hit density), takes top-N,
|
|
1106
|
-
// then expands each by --around turns of surrounding context.
|
|
227
|
+
// Drill-down step 2 in the search workflow: `search <kw>` picks a session,
|
|
228
|
+
// then `context <id> --grep <kw>` returns top-N hit turns with surrounding
|
|
229
|
+
// context, token-budgeted for AI consumption. Without --grep: first N turns.
|
|
1107
230
|
const id = argv.positional[0];
|
|
1108
231
|
if (!id)
|
|
1109
232
|
die("usage: context <session-id> [--grep KW] [--turns N] [--around M]");
|
|
1110
233
|
const f = buildFilter(argv.flags);
|
|
1111
|
-
|
|
1112
|
-
if (!s)
|
|
1113
|
-
die(`session not found: ${id}`);
|
|
234
|
+
maybeWarnOpencode(f);
|
|
1114
235
|
const grepRaw = argv.flags.grep;
|
|
1115
236
|
const grep = typeof grepRaw === "string" ? grepRaw : undefined;
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
let
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
let totalHitTurns = 0;
|
|
1133
|
-
if (grep) {
|
|
1134
|
-
const tokens = grep.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1135
|
-
if (tokens.length === 0)
|
|
1136
|
-
die("--grep requires non-empty value");
|
|
1137
|
-
const matchCount = (text) => {
|
|
1138
|
-
const hay = text.toLowerCase();
|
|
1139
|
-
if (!tokens.every((tok) => hay.includes(tok)))
|
|
1140
|
-
return 0;
|
|
1141
|
-
let n = 0;
|
|
1142
|
-
for (const tok of tokens) {
|
|
1143
|
-
let from = 0;
|
|
1144
|
-
while (true) {
|
|
1145
|
-
const idx = hay.indexOf(tok, from);
|
|
1146
|
-
if (idx === -1)
|
|
1147
|
-
break;
|
|
1148
|
-
n++;
|
|
1149
|
-
from = idx + tok.length;
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
return n;
|
|
1153
|
-
};
|
|
1154
|
-
const ranked = [];
|
|
1155
|
-
for (let i = 0; i < turns.length; i++) {
|
|
1156
|
-
const turn = turns[i];
|
|
1157
|
-
if (!turn)
|
|
1158
|
-
continue;
|
|
1159
|
-
const h = matchCount(turn.text);
|
|
1160
|
-
if (h > 0)
|
|
1161
|
-
ranked.push({ idx: i, role: turn.role, hits: h });
|
|
1162
|
-
}
|
|
1163
|
-
totalHitTurns = ranked.length;
|
|
1164
|
-
ranked.sort((a, b) => {
|
|
1165
|
-
if (a.role !== b.role)
|
|
1166
|
-
return a.role === "user" ? -1 : 1;
|
|
1167
|
-
if (b.hits !== a.hits)
|
|
1168
|
-
return b.hits - a.hits;
|
|
1169
|
-
return a.idx - b.idx;
|
|
237
|
+
if (grep?.split(/\s+/).filter(Boolean).length === 0)
|
|
238
|
+
die("--grep requires non-empty value");
|
|
239
|
+
const nTurns = parseOptionalNumberFlag(argv.flags.turns, "--turns", 3);
|
|
240
|
+
const around = parseOptionalNumberFlag(argv.flags.around, "--around", 1);
|
|
241
|
+
const maxChars = parseOptionalNumberFlag(argv.flags["max-chars"], "--max-chars", 6000);
|
|
242
|
+
const includeChildren = argv.flags["include-children"] === true;
|
|
243
|
+
let result;
|
|
244
|
+
try {
|
|
245
|
+
result = readMemContext({
|
|
246
|
+
sessionId: id,
|
|
247
|
+
filter: f,
|
|
248
|
+
grep,
|
|
249
|
+
turns: nTurns,
|
|
250
|
+
around,
|
|
251
|
+
maxChars,
|
|
252
|
+
includeChildren,
|
|
1170
253
|
});
|
|
1171
|
-
hitIndices = ranked.slice(0, nTurns).map((r) => r.idx);
|
|
1172
254
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
}
|
|
1178
|
-
// Expand each hit by `around` turns on either side; dedupe via Set.
|
|
1179
|
-
const display = new Set();
|
|
1180
|
-
for (const idx of hitIndices) {
|
|
1181
|
-
for (let j = Math.max(0, idx - around); j <= Math.min(turns.length - 1, idx + around); j++) {
|
|
1182
|
-
display.add(j);
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
const ordered = [...display].sort((a, b) => a - b);
|
|
1186
|
-
const hitSet = new Set(hitIndices);
|
|
1187
|
-
const out = [];
|
|
1188
|
-
let used = 0;
|
|
1189
|
-
for (const i of ordered) {
|
|
1190
|
-
const t = turns[i];
|
|
1191
|
-
if (!t)
|
|
1192
|
-
continue;
|
|
1193
|
-
let text = t.text;
|
|
1194
|
-
// Per-turn cap: if a single turn exceeds half the budget, truncate it so we
|
|
1195
|
-
// still fit the rest of the requested context.
|
|
1196
|
-
const cap = Math.floor(maxChars / 2);
|
|
1197
|
-
if (text.length > cap)
|
|
1198
|
-
text = text.slice(0, cap) + `\n…[+${t.text.length - cap} chars]`;
|
|
1199
|
-
if (used + text.length > maxChars && out.length > 0)
|
|
1200
|
-
break;
|
|
1201
|
-
out.push({ idx: i, role: t.role, text, is_hit: hitSet.has(i) });
|
|
1202
|
-
used += text.length;
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (error instanceof MemSessionNotFoundError)
|
|
257
|
+
die(`session not found: ${id}`);
|
|
258
|
+
throw error;
|
|
1203
259
|
}
|
|
260
|
+
const s = result.session;
|
|
1204
261
|
if (argv.flags.json) {
|
|
1205
262
|
console.log(JSON.stringify({
|
|
1206
263
|
session: s,
|
|
1207
|
-
query:
|
|
1208
|
-
total_turns:
|
|
1209
|
-
total_hit_turns: totalHitTurns,
|
|
1210
|
-
merged_children: mergedChildren,
|
|
1211
|
-
turns:
|
|
264
|
+
query: result.query,
|
|
265
|
+
total_turns: result.totalTurns,
|
|
266
|
+
total_hit_turns: result.totalHitTurns,
|
|
267
|
+
merged_children: result.mergedChildren,
|
|
268
|
+
turns: result.turns.map((t) => ({
|
|
269
|
+
idx: t.idx,
|
|
270
|
+
role: t.role,
|
|
271
|
+
text: t.text,
|
|
272
|
+
is_hit: t.isHit,
|
|
273
|
+
})),
|
|
1212
274
|
}, null, 2));
|
|
1213
275
|
return;
|
|
1214
276
|
}
|
|
277
|
+
// `hitIndices.length` from the legacy implementation — recomputed here for
|
|
278
|
+
// the human-readable header only.
|
|
279
|
+
const shown = grep
|
|
280
|
+
? Math.min(result.totalHitTurns, nTurns)
|
|
281
|
+
: Math.min(nTurns, result.totalTurns);
|
|
1215
282
|
console.log(`# context: [${s.platform}] ${s.id}`);
|
|
1216
283
|
if (s.title)
|
|
1217
284
|
console.log(`# title: ${s.title}`);
|
|
1218
285
|
if (s.cwd)
|
|
1219
286
|
console.log(`# cwd: ${shortPath(s.cwd)}`);
|
|
1220
287
|
if (grep)
|
|
1221
|
-
console.log(`# query: "${grep}" hit_turns=${totalHitTurns} showing top ${
|
|
288
|
+
console.log(`# query: "${grep}" hit_turns=${result.totalHitTurns} showing top ${shown}`);
|
|
1222
289
|
else
|
|
1223
|
-
console.log(`# no grep — showing first ${
|
|
1224
|
-
if (mergedChildren > 0)
|
|
1225
|
-
console.log(`# merged_children: ${mergedChildren}`);
|
|
1226
|
-
console.log(`# turns shown: ${
|
|
290
|
+
console.log(`# no grep — showing first ${shown} turns of ${result.totalTurns}`);
|
|
291
|
+
if (result.mergedChildren > 0)
|
|
292
|
+
console.log(`# merged_children: ${result.mergedChildren}`);
|
|
293
|
+
console.log(`# turns shown: ${result.turns.length} budget_used: ${result.budgetUsed}/${result.maxChars} chars`);
|
|
1227
294
|
console.log("");
|
|
1228
|
-
for (const t of
|
|
1229
|
-
const marker = t.
|
|
295
|
+
for (const t of result.turns) {
|
|
296
|
+
const marker = t.isHit ? " ← hit" : "";
|
|
1230
297
|
console.log(`## turn ${t.idx} (${t.role})${marker}\n`);
|
|
1231
298
|
console.log(t.text);
|
|
1232
299
|
console.log("\n---\n");
|
|
1233
300
|
}
|
|
1234
301
|
}
|
|
302
|
+
function parsePhaseFlag(raw) {
|
|
303
|
+
if (raw === undefined || raw === false)
|
|
304
|
+
return "all";
|
|
305
|
+
if (raw === "brainstorm" || raw === "implement" || raw === "all")
|
|
306
|
+
return raw;
|
|
307
|
+
die(`unknown --phase: ${String(raw)} (expected brainstorm|implement|all)`);
|
|
308
|
+
}
|
|
1235
309
|
function cmdExtract(argv) {
|
|
1236
310
|
const id = argv.positional[0];
|
|
1237
311
|
if (!id)
|
|
1238
312
|
die("usage: extract <session-id>");
|
|
1239
313
|
const f = buildFilter(argv.flags);
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
die(`session not found: ${id}`);
|
|
1243
|
-
const turns = extractDialogue(s);
|
|
314
|
+
maybeWarnOpencode(f);
|
|
315
|
+
const phase = parsePhaseFlag(argv.flags.phase);
|
|
1244
316
|
const grepRaw = argv.flags.grep;
|
|
1245
317
|
const grep = typeof grepRaw === "string" ? grepRaw.toLowerCase() : undefined;
|
|
318
|
+
let result;
|
|
319
|
+
try {
|
|
320
|
+
result = extractMemDialogue({ sessionId: id, filter: f, phase, grep });
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (error instanceof MemSessionNotFoundError)
|
|
324
|
+
die(`session not found: ${id}`);
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
for (const w of result.warnings)
|
|
328
|
+
console.error(`warning: ${w.message}`);
|
|
329
|
+
const s = result.session;
|
|
1246
330
|
if (argv.flags.json) {
|
|
1247
331
|
console.log(JSON.stringify({
|
|
1248
332
|
session: s,
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
333
|
+
phase: result.phase,
|
|
334
|
+
windows: result.windows,
|
|
335
|
+
total_turns: result.totalTurns,
|
|
336
|
+
groups: result.groups,
|
|
337
|
+
turns: result.turns,
|
|
1252
338
|
}, null, 2));
|
|
1253
339
|
return;
|
|
1254
340
|
}
|
|
@@ -1259,14 +345,18 @@ function cmdExtract(argv) {
|
|
|
1259
345
|
console.log(`# cwd: ${shortPath(s.cwd)}`);
|
|
1260
346
|
if (s.created)
|
|
1261
347
|
console.log(`# date: ${shortDate(s.created)}`);
|
|
1262
|
-
console.log(`# turns: ${turns.length}
|
|
348
|
+
console.log(`# phase: ${result.phase} turns: ${result.turns.length}/${result.totalTurns}` +
|
|
349
|
+
(grep ? ` (filtered by /${grep}/)` : "") +
|
|
350
|
+
(result.windows.length > 0 ? ` windows: ${result.windows.length}` : ""));
|
|
1263
351
|
console.log("");
|
|
1264
|
-
for (const
|
|
1265
|
-
if (
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
352
|
+
for (const g of result.groups) {
|
|
353
|
+
if (g.label !== null)
|
|
354
|
+
console.log(`--- task: ${g.label} ---\n`);
|
|
355
|
+
for (const t of g.turns) {
|
|
356
|
+
console.log(`## ${t.role === "user" ? "Human" : "Assistant"}\n`);
|
|
357
|
+
console.log(t.text);
|
|
358
|
+
console.log("\n---\n");
|
|
359
|
+
}
|
|
1270
360
|
}
|
|
1271
361
|
}
|
|
1272
362
|
function cmdHelp() {
|
|
@@ -1289,6 +379,9 @@ flags:
|
|
|
1289
379
|
--cwd <path> override the project cwd
|
|
1290
380
|
--limit N cap output (default 50)
|
|
1291
381
|
--grep KW extract / context: filter turns by keyword (multi-token AND)
|
|
382
|
+
--phase brainstorm|implement|all extract: slice by Trellis brainstorm windows
|
|
383
|
+
(default all; brainstorm = [task.py create, task.py start);
|
|
384
|
+
Claude/Codex supported; OpenCode warns + returns all)
|
|
1292
385
|
--turns N context: number of hit turns to return (default 3)
|
|
1293
386
|
--around N context: turns of surrounding context per hit (default 1)
|
|
1294
387
|
--max-chars N context: total char budget (default 6000, ~1500 tokens)
|
|
@@ -1301,6 +394,7 @@ examples:
|
|
|
1301
394
|
trellis mem list --global --platform claude --since 2026-04-01
|
|
1302
395
|
trellis mem search "session insight" --global
|
|
1303
396
|
trellis mem extract 5842592d --grep memory
|
|
397
|
+
trellis mem extract 5842592d --phase brainstorm
|
|
1304
398
|
`);
|
|
1305
399
|
}
|
|
1306
400
|
// ---------- entry ----------
|