discoclaw 0.1.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/.context/README.md +42 -0
- package/.context/architecture.md +58 -0
- package/.context/bot-setup.md +24 -0
- package/.context/dev.md +230 -0
- package/.context/discord.md +144 -0
- package/.context/memory.md +257 -0
- package/.context/ops.md +59 -0
- package/.context/pa-safety.md +47 -0
- package/.context/pa.md +118 -0
- package/.context/project.md +43 -0
- package/.context/runtime.md +253 -0
- package/.context/tasks.md +71 -0
- package/.context/tools.md +75 -0
- package/.env.example +88 -0
- package/.env.example.full +378 -0
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/beads/auto-tag.js +2 -0
- package/dist/beads/auto-tag.test.js +62 -0
- package/dist/beads/bd-cli.js +9 -0
- package/dist/beads/bd-cli.test.js +495 -0
- package/dist/beads/bead-hooks-cli.js +149 -0
- package/dist/beads/bead-sync-cli.js +5 -0
- package/dist/beads/bead-sync-cli.test.js +72 -0
- package/dist/beads/bead-sync-coordinator.js +4 -0
- package/dist/beads/bead-sync-coordinator.test.js +239 -0
- package/dist/beads/bead-sync-watcher.js +2 -0
- package/dist/beads/bead-sync-watcher.test.js +96 -0
- package/dist/beads/bead-sync.js +7 -0
- package/dist/beads/bead-sync.test.js +876 -0
- package/dist/beads/bead-thread-cache.js +8 -0
- package/dist/beads/bead-thread-cache.test.js +91 -0
- package/dist/beads/discord-sync.js +18 -0
- package/dist/beads/discord-sync.test.js +782 -0
- package/dist/beads/find-bead-by-thread.test.js +36 -0
- package/dist/beads/forum-guard.js +2 -0
- package/dist/beads/forum-guard.test.js +204 -0
- package/dist/beads/initialize.js +3 -0
- package/dist/beads/initialize.test.js +304 -0
- package/dist/beads/types.js +10 -0
- package/dist/cli/daemon-installer.js +225 -0
- package/dist/cli/daemon-installer.test.js +289 -0
- package/dist/cli/index.js +42 -0
- package/dist/cli/init-wizard.js +374 -0
- package/dist/cli/init-wizard.test.js +191 -0
- package/dist/config.js +385 -0
- package/dist/config.test.js +589 -0
- package/dist/cron/auto-tag.js +100 -0
- package/dist/cron/auto-tag.test.js +91 -0
- package/dist/cron/cadence.js +74 -0
- package/dist/cron/cadence.test.js +53 -0
- package/dist/cron/cron-sync-coordinator.js +66 -0
- package/dist/cron/cron-sync-coordinator.test.js +118 -0
- package/dist/cron/cron-sync.js +165 -0
- package/dist/cron/cron-sync.test.js +228 -0
- package/dist/cron/cron-tag-map-watcher.js +128 -0
- package/dist/cron/cron-tag-map-watcher.test.js +155 -0
- package/dist/cron/default-timezone.js +23 -0
- package/dist/cron/default-timezone.test.js +30 -0
- package/dist/cron/discord-sync.js +205 -0
- package/dist/cron/discord-sync.test.js +353 -0
- package/dist/cron/executor.js +303 -0
- package/dist/cron/executor.test.js +614 -0
- package/dist/cron/forum-sync.js +347 -0
- package/dist/cron/forum-sync.test.js +539 -0
- package/dist/cron/job-lock.js +164 -0
- package/dist/cron/job-lock.test.js +178 -0
- package/dist/cron/parser.js +68 -0
- package/dist/cron/parser.test.js +115 -0
- package/dist/cron/run-control.js +24 -0
- package/dist/cron/run-control.test.js +27 -0
- package/dist/cron/run-stats.js +265 -0
- package/dist/cron/run-stats.test.js +160 -0
- package/dist/cron/scheduler.js +97 -0
- package/dist/cron/scheduler.test.js +112 -0
- package/dist/cron/tag-map.js +47 -0
- package/dist/cron/tag-map.test.js +64 -0
- package/dist/cron/types.js +1 -0
- package/dist/discoclaw-plan-format.test.js +137 -0
- package/dist/discoclaw-recipe-format.test.js +137 -0
- package/dist/discord/abort-registry.js +70 -0
- package/dist/discord/action-categories.js +36 -0
- package/dist/discord/action-types.js +1 -0
- package/dist/discord/action-utils.js +58 -0
- package/dist/discord/action-utils.test.js +58 -0
- package/dist/discord/actions-beads.js +1 -0
- package/dist/discord/actions-beads.test.js +372 -0
- package/dist/discord/actions-bot-profile.js +107 -0
- package/dist/discord/actions-bot-profile.test.js +138 -0
- package/dist/discord/actions-channels.js +427 -0
- package/dist/discord/actions-channels.test.js +697 -0
- package/dist/discord/actions-config.js +173 -0
- package/dist/discord/actions-config.test.js +322 -0
- package/dist/discord/actions-crons.js +586 -0
- package/dist/discord/actions-crons.test.js +499 -0
- package/dist/discord/actions-defer.js +60 -0
- package/dist/discord/actions-defer.test.js +134 -0
- package/dist/discord/actions-forge.js +134 -0
- package/dist/discord/actions-forge.test.js +206 -0
- package/dist/discord/actions-guild.js +301 -0
- package/dist/discord/actions-guild.test.js +386 -0
- package/dist/discord/actions-memory.js +106 -0
- package/dist/discord/actions-memory.test.js +248 -0
- package/dist/discord/actions-messaging.js +401 -0
- package/dist/discord/actions-messaging.test.js +738 -0
- package/dist/discord/actions-moderation.js +65 -0
- package/dist/discord/actions-moderation.test.js +88 -0
- package/dist/discord/actions-plan.js +445 -0
- package/dist/discord/actions-plan.test.js +610 -0
- package/dist/discord/actions-poll.js +38 -0
- package/dist/discord/actions-poll.test.js +93 -0
- package/dist/discord/actions-tasks.js +3 -0
- package/dist/discord/actions-tasks.test.js +418 -0
- package/dist/discord/actions.js +600 -0
- package/dist/discord/actions.test.js +522 -0
- package/dist/discord/allowed-mentions.js +3 -0
- package/dist/discord/allowed-mentions.test.js +17 -0
- package/dist/discord/allowlist.js +29 -0
- package/dist/discord/allowlist.test.js +24 -0
- package/dist/discord/audit-handler.js +191 -0
- package/dist/discord/audit-handler.test.js +361 -0
- package/dist/discord/bot.js +141 -0
- package/dist/discord/channel-context.js +181 -0
- package/dist/discord/defer-scheduler.js +45 -0
- package/dist/discord/destructive-confirmation.js +128 -0
- package/dist/discord/destructive-confirmation.test.js +49 -0
- package/dist/discord/discord-plan-auto-implement.test.js +18 -0
- package/dist/discord/durable-memory.js +145 -0
- package/dist/discord/durable-memory.test.js +281 -0
- package/dist/discord/durable-write-queue.js +4 -0
- package/dist/discord/file-download.js +308 -0
- package/dist/discord/file-download.test.js +303 -0
- package/dist/discord/forge-audit-verdict.js +140 -0
- package/dist/discord/forge-auto-implement.js +80 -0
- package/dist/discord/forge-auto-implement.test.js +110 -0
- package/dist/discord/forge-commands.js +698 -0
- package/dist/discord/forge-commands.test.js +1606 -0
- package/dist/discord/forge-plan-registry.js +68 -0
- package/dist/discord/forge-plan-registry.test.js +127 -0
- package/dist/discord/forum-count-sync.js +130 -0
- package/dist/discord/forum-count-sync.test.js +200 -0
- package/dist/discord/health-command.js +98 -0
- package/dist/discord/health-command.test.js +195 -0
- package/dist/discord/help-command.js +22 -0
- package/dist/discord/help-command.test.js +49 -0
- package/dist/discord/image-download.js +201 -0
- package/dist/discord/image-download.test.js +499 -0
- package/dist/discord/inflight-replies.js +228 -0
- package/dist/discord/inflight-replies.test.js +295 -0
- package/dist/discord/json-extract.js +110 -0
- package/dist/discord/keyed-queue.js +22 -0
- package/dist/discord/memory-commands.js +85 -0
- package/dist/discord/memory-commands.test.js +159 -0
- package/dist/discord/memory-timing.integration.test.js +159 -0
- package/dist/discord/message-coordinator.js +2347 -0
- package/dist/discord/message-coordinator.onboarding.test.js +183 -0
- package/dist/discord/message-coordinator.plan-run.test.js +264 -0
- package/dist/discord/message-history.js +53 -0
- package/dist/discord/message-history.test.js +95 -0
- package/dist/discord/models-command.js +59 -0
- package/dist/discord/models-command.test.js +150 -0
- package/dist/discord/nickname.test.js +76 -0
- package/dist/discord/onboarding-completion.js +55 -0
- package/dist/discord/onboarding-completion.test.js +176 -0
- package/dist/discord/output-common.js +178 -0
- package/dist/discord/output-common.test.js +198 -0
- package/dist/discord/output-utils.js +156 -0
- package/dist/discord/parse-identity-name.test.js +129 -0
- package/dist/discord/plan-commands.js +612 -0
- package/dist/discord/plan-commands.test.js +1622 -0
- package/dist/discord/plan-manager.js +1491 -0
- package/dist/discord/plan-manager.test.js +2380 -0
- package/dist/discord/plan-parser.js +110 -0
- package/dist/discord/plan-parser.test.js +63 -0
- package/dist/discord/plan-run-phase-start.js +20 -0
- package/dist/discord/plan-run-phase-start.test.js +29 -0
- package/dist/discord/platform-message.js +45 -0
- package/dist/discord/platform-message.test.js +110 -0
- package/dist/discord/prompt-common.js +240 -0
- package/dist/discord/prompt-common.test.js +423 -0
- package/dist/discord/reaction-handler.js +691 -0
- package/dist/discord/reaction-handler.test.js +1574 -0
- package/dist/discord/reaction-prompts.js +118 -0
- package/dist/discord/reaction-prompts.test.js +253 -0
- package/dist/discord/reply-reference.js +66 -0
- package/dist/discord/reply-reference.test.js +125 -0
- package/dist/discord/restart-command.js +143 -0
- package/dist/discord/restart-command.test.js +196 -0
- package/dist/discord/runtime-utils.js +43 -0
- package/dist/discord/runtime-utils.test.js +112 -0
- package/dist/discord/session-key.js +7 -0
- package/dist/discord/session-key.test.js +13 -0
- package/dist/discord/shortterm-memory.js +166 -0
- package/dist/discord/shortterm-memory.test.js +345 -0
- package/dist/discord/shutdown-context.js +122 -0
- package/dist/discord/shutdown-context.test.js +279 -0
- package/dist/discord/startup-profile.test.js +214 -0
- package/dist/discord/status-channel.js +190 -0
- package/dist/discord/status-channel.test.js +282 -0
- package/dist/discord/status-command.js +206 -0
- package/dist/discord/status-command.test.js +341 -0
- package/dist/discord/streaming-progress.js +107 -0
- package/dist/discord/streaming-progress.test.js +93 -0
- package/dist/discord/summarizer.js +89 -0
- package/dist/discord/summarizer.test.js +245 -0
- package/dist/discord/system-bootstrap.js +396 -0
- package/dist/discord/system-bootstrap.test.js +724 -0
- package/dist/discord/thread-context.js +169 -0
- package/dist/discord/thread-context.test.js +386 -0
- package/dist/discord/tool-aware-queue.js +116 -0
- package/dist/discord/tool-aware-queue.test.js +180 -0
- package/dist/discord/update-command.js +127 -0
- package/dist/discord/update-command.test.js +275 -0
- package/dist/discord/user-errors.js +40 -0
- package/dist/discord/user-errors.test.js +31 -0
- package/dist/discord/user-turn-to-durable.js +111 -0
- package/dist/discord/user-turn-to-durable.test.js +273 -0
- package/dist/discord-followup.test.js +677 -0
- package/dist/discord.channel-context.test.js +95 -0
- package/dist/discord.fail-closed.test.js +199 -0
- package/dist/discord.health-command.integration.test.js +140 -0
- package/dist/discord.js +190 -0
- package/dist/discord.prompt-context.test.js +1431 -0
- package/dist/discord.render.test.js +621 -0
- package/dist/discord.status-wiring.test.js +187 -0
- package/dist/engine/claudeCli.js +137 -0
- package/dist/engine/types.js +1 -0
- package/dist/group-queue.js +25 -0
- package/dist/health/credential-check.js +175 -0
- package/dist/health/credential-check.test.js +401 -0
- package/dist/health/startup-healing.js +139 -0
- package/dist/health/startup-healing.test.js +298 -0
- package/dist/identity.js +36 -0
- package/dist/index.js +1378 -0
- package/dist/logging/logger-like.js +1 -0
- package/dist/observability/memory-sampler.js +51 -0
- package/dist/observability/memory-sampler.test.js +93 -0
- package/dist/observability/metrics.js +88 -0
- package/dist/observability/metrics.test.js +42 -0
- package/dist/onboarding/onboarding-flow.js +246 -0
- package/dist/onboarding/onboarding-flow.test.js +238 -0
- package/dist/onboarding/onboarding-writer.js +102 -0
- package/dist/onboarding/onboarding-writer.test.js +143 -0
- package/dist/pidlock.js +187 -0
- package/dist/pidlock.test.js +128 -0
- package/dist/pipeline/engine.js +206 -0
- package/dist/pipeline/engine.test.js +771 -0
- package/dist/root-policy.js +21 -0
- package/dist/root-policy.test.js +55 -0
- package/dist/runtime/claude-code-cli.js +35 -0
- package/dist/runtime/claude-code-cli.test.js +1199 -0
- package/dist/runtime/cli-adapter.js +584 -0
- package/dist/runtime/cli-output-parsers.js +108 -0
- package/dist/runtime/cli-shared.js +96 -0
- package/dist/runtime/cli-shared.test.js +104 -0
- package/dist/runtime/cli-strategy.js +6 -0
- package/dist/runtime/codex-cli.js +16 -0
- package/dist/runtime/codex-cli.test.js +862 -0
- package/dist/runtime/concurrency-limit.js +80 -0
- package/dist/runtime/concurrency-limit.test.js +137 -0
- package/dist/runtime/gemini-cli.js +16 -0
- package/dist/runtime/gemini-cli.test.js +413 -0
- package/dist/runtime/long-running-process.js +415 -0
- package/dist/runtime/long-running-process.test.js +318 -0
- package/dist/runtime/model-smoke-helpers.js +160 -0
- package/dist/runtime/model-smoke.test.js +194 -0
- package/dist/runtime/model-tiers.js +33 -0
- package/dist/runtime/model-tiers.test.js +65 -0
- package/dist/runtime/openai-auth.js +151 -0
- package/dist/runtime/openai-auth.test.js +361 -0
- package/dist/runtime/openai-compat.js +178 -0
- package/dist/runtime/openai-compat.test.js +449 -0
- package/dist/runtime/process-pool.js +93 -0
- package/dist/runtime/process-pool.test.js +148 -0
- package/dist/runtime/registry.js +15 -0
- package/dist/runtime/registry.test.js +47 -0
- package/dist/runtime/session-scanner.js +186 -0
- package/dist/runtime/session-scanner.test.js +257 -0
- package/dist/runtime/strategies/claude-strategy.js +193 -0
- package/dist/runtime/strategies/codex-strategy.js +161 -0
- package/dist/runtime/strategies/gemini-strategy.js +64 -0
- package/dist/runtime/strategies/template-strategy.js +85 -0
- package/dist/runtime/tool-capabilities.js +27 -0
- package/dist/runtime/tool-capabilities.test.js +24 -0
- package/dist/runtime/tool-labels.js +48 -0
- package/dist/runtime/types.js +2 -0
- package/dist/sessionManager.js +47 -0
- package/dist/sessions.js +18 -0
- package/dist/tasks/architecture-contract.js +33 -0
- package/dist/tasks/architecture-contract.test.js +90 -0
- package/dist/tasks/auto-tag.js +50 -0
- package/dist/tasks/auto-tag.test.js +64 -0
- package/dist/tasks/bd-cli.js +164 -0
- package/dist/tasks/bd-cli.test.js +359 -0
- package/dist/tasks/bead-sync.js +1 -0
- package/dist/tasks/context-summary.js +27 -0
- package/dist/tasks/discord-sync.js +3 -0
- package/dist/tasks/discord-sync.test.js +685 -0
- package/dist/tasks/discord-types.js +4 -0
- package/dist/tasks/find-task-by-thread.test.js +36 -0
- package/dist/tasks/forum-guard.js +81 -0
- package/dist/tasks/forum-guard.test.js +192 -0
- package/dist/tasks/initialize.js +77 -0
- package/dist/tasks/initialize.test.js +263 -0
- package/dist/tasks/logger-types.js +1 -0
- package/dist/tasks/metrics-types.js +3 -0
- package/dist/tasks/migrate.js +33 -0
- package/dist/tasks/migrate.test.js +156 -0
- package/dist/tasks/path-defaults.js +67 -0
- package/dist/tasks/path-defaults.test.js +73 -0
- package/dist/tasks/runtime-types.js +1 -0
- package/dist/tasks/service.js +33 -0
- package/dist/tasks/service.test.js +51 -0
- package/dist/tasks/store.js +238 -0
- package/dist/tasks/store.test.js +417 -0
- package/dist/tasks/sync-context.js +1 -0
- package/dist/tasks/sync-contract.js +24 -0
- package/dist/tasks/sync-contract.test.js +25 -0
- package/dist/tasks/sync-coordinator-metrics.js +41 -0
- package/dist/tasks/sync-coordinator-retries.js +71 -0
- package/dist/tasks/sync-coordinator.js +96 -0
- package/dist/tasks/sync-coordinator.test.js +501 -0
- package/dist/tasks/sync-types.js +1 -0
- package/dist/tasks/sync-watcher.js +27 -0
- package/dist/tasks/sync-watcher.test.js +92 -0
- package/dist/tasks/tag-map.js +36 -0
- package/dist/tasks/tag-map.test.js +54 -0
- package/dist/tasks/task-action-contract.js +16 -0
- package/dist/tasks/task-action-contract.test.js +16 -0
- package/dist/tasks/task-action-executor.js +18 -0
- package/dist/tasks/task-action-executor.test.js +420 -0
- package/dist/tasks/task-action-mutation-helpers.js +17 -0
- package/dist/tasks/task-action-mutations.js +151 -0
- package/dist/tasks/task-action-prompt.js +62 -0
- package/dist/tasks/task-action-read-ops.js +73 -0
- package/dist/tasks/task-action-runner-types.js +1 -0
- package/dist/tasks/task-action-thread-sync.js +82 -0
- package/dist/tasks/task-actions.js +3 -0
- package/dist/tasks/task-cli.js +227 -0
- package/dist/tasks/task-context.js +1 -0
- package/dist/tasks/task-lifecycle.js +46 -0
- package/dist/tasks/task-lifecycle.test.js +35 -0
- package/dist/tasks/task-sync-apply-plan.js +95 -0
- package/dist/tasks/task-sync-apply-types.js +12 -0
- package/dist/tasks/task-sync-apply.js +319 -0
- package/dist/tasks/task-sync-cli.js +89 -0
- package/dist/tasks/task-sync-cli.test.js +70 -0
- package/dist/tasks/task-sync-engine.js +88 -0
- package/dist/tasks/task-sync-engine.test.js +934 -0
- package/dist/tasks/task-sync-phase-apply.js +171 -0
- package/dist/tasks/task-sync-pipeline.js +2 -0
- package/dist/tasks/task-sync-pipeline.test.js +265 -0
- package/dist/tasks/task-sync-reconcile-plan.js +182 -0
- package/dist/tasks/task-sync-reconcile.js +144 -0
- package/dist/tasks/task-sync.js +56 -0
- package/dist/tasks/task-sync.test.js +86 -0
- package/dist/tasks/thread-cache.js +42 -0
- package/dist/tasks/thread-cache.test.js +89 -0
- package/dist/tasks/thread-contracts.test.js +711 -0
- package/dist/tasks/thread-forum-ops.js +68 -0
- package/dist/tasks/thread-helpers.js +86 -0
- package/dist/tasks/thread-helpers.test.js +33 -0
- package/dist/tasks/thread-lifecycle-ops.js +144 -0
- package/dist/tasks/thread-ops-shared.js +21 -0
- package/dist/tasks/thread-ops.js +2 -0
- package/dist/tasks/types.js +20 -0
- package/dist/tasks/types.test.js +60 -0
- package/dist/test-setup.js +11 -0
- package/dist/test-setup.test.js +42 -0
- package/dist/transport/types.js +1 -0
- package/dist/validate.js +41 -0
- package/dist/validate.test.js +94 -0
- package/dist/version.js +15 -0
- package/dist/version.test.js +31 -0
- package/dist/webhook/server.js +199 -0
- package/dist/webhook/server.test.js +460 -0
- package/dist/workspace-bootstrap.js +135 -0
- package/dist/workspace-bootstrap.test.js +514 -0
- package/dist/workspace-permissions.js +134 -0
- package/dist/workspace-permissions.test.js +181 -0
- package/package.json +74 -0
- package/scripts/cron/cron-tag-map.json +9 -0
- package/scripts/tasks/tag-map.json +10 -0
- package/systemd/discoclaw.service +19 -0
- package/templates/recipes/integration.discoclaw-recipe.md +171 -0
- package/templates/workspace/AGENTS.md +217 -0
- package/templates/workspace/BOOTSTRAP.md +1 -0
- package/templates/workspace/HEARTBEAT.md +10 -0
- package/templates/workspace/IDENTITY.md +16 -0
- package/templates/workspace/MEMORY.md +24 -0
- package/templates/workspace/SOUL.md +52 -0
- package/templates/workspace/TOOLS.md +304 -0
- package/templates/workspace/USER.md +37 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { SessionFileScanner } from './session-scanner.js';
|
|
6
|
+
import { toolActivityLabel } from './tool-labels.js';
|
|
7
|
+
let tmpDir;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
tmpDir = await fsp.mkdtemp(path.join('/tmp', 'scanner-test-'));
|
|
10
|
+
// Mock os.homedir() to point at our temp dir so the scanner finds session files there.
|
|
11
|
+
vi.spyOn(os, 'homedir').mockReturnValue(tmpDir);
|
|
12
|
+
});
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
await fsp.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
16
|
+
});
|
|
17
|
+
function sessionDir(cwd) {
|
|
18
|
+
const escaped = cwd.replace(/\//g, '-');
|
|
19
|
+
return path.join(tmpDir, '.claude', 'projects', escaped);
|
|
20
|
+
}
|
|
21
|
+
async function ensureSessionFile(cwd, sessionId) {
|
|
22
|
+
const dir = sessionDir(cwd);
|
|
23
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
24
|
+
const filePath = path.join(dir, `${sessionId}.jsonl`);
|
|
25
|
+
await fsp.writeFile(filePath, '', 'utf8');
|
|
26
|
+
return filePath;
|
|
27
|
+
}
|
|
28
|
+
function makeToolUse(id, name, input) {
|
|
29
|
+
return JSON.stringify({
|
|
30
|
+
type: 'assistant',
|
|
31
|
+
message: {
|
|
32
|
+
content: [
|
|
33
|
+
{ type: 'tool_use', id, name, input: input ?? {} },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function makeToolResult(toolUseId, isError = false) {
|
|
39
|
+
return JSON.stringify({
|
|
40
|
+
type: 'user',
|
|
41
|
+
message: {
|
|
42
|
+
content: [
|
|
43
|
+
{ type: 'tool_result', tool_use_id: toolUseId, is_error: isError },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
describe('SessionFileScanner', () => {
|
|
49
|
+
it('detects tool_use and emits tool_start', async () => {
|
|
50
|
+
const cwd = '/home/test/code/proj';
|
|
51
|
+
const sessionId = 'test-session-1';
|
|
52
|
+
const filePath = await ensureSessionFile(cwd, sessionId);
|
|
53
|
+
const events = [];
|
|
54
|
+
const scanner = new SessionFileScanner({ sessionId, cwd }, { onEvent: (evt) => events.push(evt) });
|
|
55
|
+
await scanner.start();
|
|
56
|
+
// Write a tool_use line after start.
|
|
57
|
+
await fsp.appendFile(filePath, makeToolUse('block-1', 'Read', { file_path: '/tmp/foo.ts' }) + '\n');
|
|
58
|
+
// Give the watcher/poll time to pick it up.
|
|
59
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
60
|
+
scanner.stop();
|
|
61
|
+
const toolStarts = events.filter((e) => e.type === 'tool_start');
|
|
62
|
+
expect(toolStarts).toHaveLength(1);
|
|
63
|
+
expect(toolStarts[0]).toMatchObject({
|
|
64
|
+
type: 'tool_start',
|
|
65
|
+
name: 'Read',
|
|
66
|
+
input: { file_path: '/tmp/foo.ts' },
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
it('detects tool_result and emits tool_end with correct name/ok', async () => {
|
|
70
|
+
const cwd = '/home/test/code/proj';
|
|
71
|
+
const sessionId = 'test-session-2';
|
|
72
|
+
const filePath = await ensureSessionFile(cwd, sessionId);
|
|
73
|
+
const events = [];
|
|
74
|
+
const scanner = new SessionFileScanner({ sessionId, cwd }, { onEvent: (evt) => events.push(evt) });
|
|
75
|
+
await scanner.start();
|
|
76
|
+
await fsp.appendFile(filePath, makeToolUse('block-2', 'Bash') + '\n');
|
|
77
|
+
await fsp.appendFile(filePath, makeToolResult('block-2', false) + '\n');
|
|
78
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
79
|
+
scanner.stop();
|
|
80
|
+
const toolEnds = events.filter((e) => e.type === 'tool_end');
|
|
81
|
+
expect(toolEnds).toHaveLength(1);
|
|
82
|
+
expect(toolEnds[0]).toMatchObject({
|
|
83
|
+
type: 'tool_end',
|
|
84
|
+
name: 'Bash',
|
|
85
|
+
ok: true,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
it('handles tool_result with is_error=true', async () => {
|
|
89
|
+
const cwd = '/home/test/code/proj';
|
|
90
|
+
const sessionId = 'test-session-err';
|
|
91
|
+
const filePath = await ensureSessionFile(cwd, sessionId);
|
|
92
|
+
const events = [];
|
|
93
|
+
const scanner = new SessionFileScanner({ sessionId, cwd }, { onEvent: (evt) => events.push(evt) });
|
|
94
|
+
await scanner.start();
|
|
95
|
+
await fsp.appendFile(filePath, makeToolUse('block-e', 'Edit') + '\n');
|
|
96
|
+
await fsp.appendFile(filePath, makeToolResult('block-e', true) + '\n');
|
|
97
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
98
|
+
scanner.stop();
|
|
99
|
+
const toolEnds = events.filter((e) => e.type === 'tool_end');
|
|
100
|
+
expect(toolEnds).toHaveLength(1);
|
|
101
|
+
expect(toolEnds[0]).toMatchObject({
|
|
102
|
+
type: 'tool_end',
|
|
103
|
+
name: 'Edit',
|
|
104
|
+
ok: false,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
it('handles multiple sequential tools', async () => {
|
|
108
|
+
const cwd = '/home/test/code/proj';
|
|
109
|
+
const sessionId = 'test-session-3';
|
|
110
|
+
const filePath = await ensureSessionFile(cwd, sessionId);
|
|
111
|
+
const events = [];
|
|
112
|
+
const scanner = new SessionFileScanner({ sessionId, cwd }, { onEvent: (evt) => events.push(evt) });
|
|
113
|
+
await scanner.start();
|
|
114
|
+
await fsp.appendFile(filePath, makeToolUse('b1', 'Read') + '\n' +
|
|
115
|
+
makeToolResult('b1') + '\n' +
|
|
116
|
+
makeToolUse('b2', 'Bash') + '\n' +
|
|
117
|
+
makeToolResult('b2') + '\n');
|
|
118
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
119
|
+
scanner.stop();
|
|
120
|
+
const starts = events.filter((e) => e.type === 'tool_start');
|
|
121
|
+
const ends = events.filter((e) => e.type === 'tool_end');
|
|
122
|
+
expect(starts).toHaveLength(2);
|
|
123
|
+
expect(ends).toHaveLength(2);
|
|
124
|
+
expect(starts[0]).toMatchObject({ name: 'Read' });
|
|
125
|
+
expect(starts[1]).toMatchObject({ name: 'Bash' });
|
|
126
|
+
expect(ends[0]).toMatchObject({ name: 'Read' });
|
|
127
|
+
expect(ends[1]).toMatchObject({ name: 'Bash' });
|
|
128
|
+
});
|
|
129
|
+
it('handles partial line buffering', async () => {
|
|
130
|
+
const cwd = '/home/test/code/proj';
|
|
131
|
+
const sessionId = 'test-session-4';
|
|
132
|
+
const filePath = await ensureSessionFile(cwd, sessionId);
|
|
133
|
+
const events = [];
|
|
134
|
+
const scanner = new SessionFileScanner({ sessionId, cwd }, { onEvent: (evt) => events.push(evt) });
|
|
135
|
+
await scanner.start();
|
|
136
|
+
// Write a partial line (no newline).
|
|
137
|
+
const fullLine = makeToolUse('b-partial', 'Grep');
|
|
138
|
+
const half1 = fullLine.slice(0, Math.floor(fullLine.length / 2));
|
|
139
|
+
const half2 = fullLine.slice(Math.floor(fullLine.length / 2));
|
|
140
|
+
await fsp.appendFile(filePath, half1);
|
|
141
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
142
|
+
// No events yet — line is incomplete.
|
|
143
|
+
expect(events.filter((e) => e.type === 'tool_start')).toHaveLength(0);
|
|
144
|
+
// Complete the line.
|
|
145
|
+
await fsp.appendFile(filePath, half2 + '\n');
|
|
146
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
147
|
+
scanner.stop();
|
|
148
|
+
const starts = events.filter((e) => e.type === 'tool_start');
|
|
149
|
+
expect(starts).toHaveLength(1);
|
|
150
|
+
expect(starts[0]).toMatchObject({ name: 'Grep' });
|
|
151
|
+
});
|
|
152
|
+
it('degrades gracefully when file does not exist', async () => {
|
|
153
|
+
const events = [];
|
|
154
|
+
const scanner = new SessionFileScanner({ sessionId: 'nonexistent', cwd: '/nonexistent/path' }, { onEvent: (evt) => events.push(evt) });
|
|
155
|
+
// Should not throw, should resolve within ~10s.
|
|
156
|
+
// We set a shorter timeout by testing that it doesn't crash.
|
|
157
|
+
await scanner.start();
|
|
158
|
+
scanner.stop();
|
|
159
|
+
expect(events).toHaveLength(0);
|
|
160
|
+
}, 15_000);
|
|
161
|
+
it('skips pre-existing content', async () => {
|
|
162
|
+
const cwd = '/home/test/code/proj';
|
|
163
|
+
const sessionId = 'test-session-5';
|
|
164
|
+
const filePath = await ensureSessionFile(cwd, sessionId);
|
|
165
|
+
const events = [];
|
|
166
|
+
// Write content before scanner starts.
|
|
167
|
+
await fsp.appendFile(filePath, makeToolUse('old-1', 'Read') + '\n');
|
|
168
|
+
await fsp.appendFile(filePath, makeToolResult('old-1') + '\n');
|
|
169
|
+
const scanner = new SessionFileScanner({ sessionId, cwd }, { onEvent: (evt) => events.push(evt) });
|
|
170
|
+
await scanner.start();
|
|
171
|
+
// Write new content after scanner starts.
|
|
172
|
+
await fsp.appendFile(filePath, makeToolUse('new-1', 'Bash') + '\n');
|
|
173
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
174
|
+
scanner.stop();
|
|
175
|
+
const starts = events.filter((e) => e.type === 'tool_start');
|
|
176
|
+
expect(starts).toHaveLength(1);
|
|
177
|
+
expect(starts[0]).toMatchObject({ name: 'Bash' });
|
|
178
|
+
});
|
|
179
|
+
it('emits no events after stop()', async () => {
|
|
180
|
+
const cwd = '/home/test/code/proj';
|
|
181
|
+
const sessionId = 'test-session-6';
|
|
182
|
+
const filePath = await ensureSessionFile(cwd, sessionId);
|
|
183
|
+
const events = [];
|
|
184
|
+
const scanner = new SessionFileScanner({ sessionId, cwd }, { onEvent: (evt) => events.push(evt) });
|
|
185
|
+
await scanner.start();
|
|
186
|
+
scanner.stop();
|
|
187
|
+
const countAfterStop = events.length;
|
|
188
|
+
// Write content after stop.
|
|
189
|
+
await fsp.appendFile(filePath, makeToolUse('late-1', 'Write') + '\n');
|
|
190
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
191
|
+
expect(events).toHaveLength(countAfterStop);
|
|
192
|
+
});
|
|
193
|
+
it('stop() emits tool_end for still-active tools', async () => {
|
|
194
|
+
const cwd = '/home/test/code/proj';
|
|
195
|
+
const sessionId = 'test-session-7';
|
|
196
|
+
const filePath = await ensureSessionFile(cwd, sessionId);
|
|
197
|
+
const events = [];
|
|
198
|
+
const scanner = new SessionFileScanner({ sessionId, cwd }, { onEvent: (evt) => events.push(evt) });
|
|
199
|
+
await scanner.start();
|
|
200
|
+
// Start a tool but don't finish it.
|
|
201
|
+
await fsp.appendFile(filePath, makeToolUse('dangling', 'Bash') + '\n');
|
|
202
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
203
|
+
scanner.stop();
|
|
204
|
+
const ends = events.filter((e) => e.type === 'tool_end');
|
|
205
|
+
expect(ends).toHaveLength(1);
|
|
206
|
+
expect(ends[0]).toMatchObject({ name: 'Bash', ok: true });
|
|
207
|
+
});
|
|
208
|
+
it('concurrent readNewBytes calls do not produce duplicate events', async () => {
|
|
209
|
+
const cwd = '/home/test/code/proj';
|
|
210
|
+
const sessionId = 'test-session-concurrent';
|
|
211
|
+
const filePath = await ensureSessionFile(cwd, sessionId);
|
|
212
|
+
const events = [];
|
|
213
|
+
const scanner = new SessionFileScanner({ sessionId, cwd }, { onEvent: (evt) => events.push(evt) });
|
|
214
|
+
await scanner.start();
|
|
215
|
+
// Write a tool_use line.
|
|
216
|
+
await fsp.appendFile(filePath, makeToolUse('conc-1', 'Read') + '\n');
|
|
217
|
+
// Trigger readNewBytes concurrently by calling the scanner's
|
|
218
|
+
// internal method multiple times. Access via the poll/watch mechanism
|
|
219
|
+
// by triggering a rapid series of file changes.
|
|
220
|
+
await fsp.appendFile(filePath, makeToolUse('conc-2', 'Bash') + '\n');
|
|
221
|
+
await fsp.appendFile(filePath, makeToolResult('conc-1') + '\n');
|
|
222
|
+
await fsp.appendFile(filePath, makeToolResult('conc-2') + '\n');
|
|
223
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
224
|
+
scanner.stop();
|
|
225
|
+
// Verify each tool_start appears exactly once.
|
|
226
|
+
const starts = events.filter((e) => e.type === 'tool_start');
|
|
227
|
+
const readStarts = starts.filter((e) => e.type === 'tool_start' && e.name === 'Read');
|
|
228
|
+
const bashStarts = starts.filter((e) => e.type === 'tool_start' && e.name === 'Bash');
|
|
229
|
+
expect(readStarts).toHaveLength(1);
|
|
230
|
+
expect(bashStarts).toHaveLength(1);
|
|
231
|
+
// Verify each tool_end appears exactly once.
|
|
232
|
+
const ends = events.filter((e) => e.type === 'tool_end');
|
|
233
|
+
expect(ends).toHaveLength(2);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
describe('toolActivityLabel', () => {
|
|
237
|
+
it('returns file-specific label for Read', () => {
|
|
238
|
+
expect(toolActivityLabel('Read', { file_path: '/home/user/code/proj/src/index.ts' }))
|
|
239
|
+
.toBe('Reading .../src/index.ts');
|
|
240
|
+
});
|
|
241
|
+
it('returns generic label for Read without input', () => {
|
|
242
|
+
expect(toolActivityLabel('Read')).toBe('Reading file...');
|
|
243
|
+
});
|
|
244
|
+
it('returns label for Bash', () => {
|
|
245
|
+
expect(toolActivityLabel('Bash')).toBe('Running command...');
|
|
246
|
+
});
|
|
247
|
+
it('returns label for Grep', () => {
|
|
248
|
+
expect(toolActivityLabel('Grep')).toBe('Searching content...');
|
|
249
|
+
});
|
|
250
|
+
it('returns fallback for unknown tools', () => {
|
|
251
|
+
expect(toolActivityLabel('CustomTool')).toBe('Running CustomTool...');
|
|
252
|
+
});
|
|
253
|
+
it('handles short paths without truncation', () => {
|
|
254
|
+
expect(toolActivityLabel('Read', { file_path: 'src/foo.ts' }))
|
|
255
|
+
.toBe('Reading src/foo.ts');
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Claude Code CLI adapter strategy.
|
|
2
|
+
// Provides model-specific logic for the universal CLI adapter factory.
|
|
3
|
+
import { extractResultText, extractResultContentBlocks } from '../cli-output-parsers.js';
|
|
4
|
+
const toolState = new WeakMap();
|
|
5
|
+
function getToolState(ctx) {
|
|
6
|
+
let s = toolState.get(ctx);
|
|
7
|
+
if (!s) {
|
|
8
|
+
s = { activeTools: new Map(), inputBufs: new Map() };
|
|
9
|
+
toolState.set(ctx, s);
|
|
10
|
+
}
|
|
11
|
+
return s;
|
|
12
|
+
}
|
|
13
|
+
export const claudeStrategy = {
|
|
14
|
+
id: 'claude_code',
|
|
15
|
+
binaryDefault: 'claude',
|
|
16
|
+
defaultModel: 'opus',
|
|
17
|
+
capabilities: [
|
|
18
|
+
'streaming_text',
|
|
19
|
+
'sessions',
|
|
20
|
+
'workspace_instructions',
|
|
21
|
+
'tools_exec',
|
|
22
|
+
'tools_fs',
|
|
23
|
+
'tools_web',
|
|
24
|
+
'mcp',
|
|
25
|
+
],
|
|
26
|
+
multiTurnMode: 'process-pool',
|
|
27
|
+
getOutputMode(ctx, opts) {
|
|
28
|
+
// Images require stream-json for content block parsing.
|
|
29
|
+
if (ctx.useStdin)
|
|
30
|
+
return 'jsonl';
|
|
31
|
+
return opts.outputFormat === 'stream-json' ? 'jsonl' : 'text';
|
|
32
|
+
},
|
|
33
|
+
buildArgs(ctx, opts) {
|
|
34
|
+
const { params, useStdin, hasImages } = ctx;
|
|
35
|
+
const effectiveOutputFormat = useStdin ? 'stream-json' : (opts.outputFormat ?? 'text');
|
|
36
|
+
const args = ['-p', '--model', params.model];
|
|
37
|
+
if (opts.dangerouslySkipPermissions) {
|
|
38
|
+
args.push('--dangerously-skip-permissions');
|
|
39
|
+
}
|
|
40
|
+
if (opts.strictMcpConfig) {
|
|
41
|
+
args.push('--strict-mcp-config');
|
|
42
|
+
}
|
|
43
|
+
if (opts.fallbackModel) {
|
|
44
|
+
args.push('--fallback-model', opts.fallbackModel);
|
|
45
|
+
}
|
|
46
|
+
if (opts.maxBudgetUsd != null) {
|
|
47
|
+
args.push('--max-budget-usd', String(opts.maxBudgetUsd));
|
|
48
|
+
}
|
|
49
|
+
if (opts.appendSystemPrompt) {
|
|
50
|
+
args.push('--append-system-prompt', opts.appendSystemPrompt);
|
|
51
|
+
}
|
|
52
|
+
if (opts.debugFile && opts.debugFile.trim()) {
|
|
53
|
+
args.push('--debug-file', opts.debugFile.trim());
|
|
54
|
+
}
|
|
55
|
+
if (opts.verbose) {
|
|
56
|
+
args.push('--verbose');
|
|
57
|
+
}
|
|
58
|
+
if (params.sessionId) {
|
|
59
|
+
args.push('--session-id', params.sessionId);
|
|
60
|
+
}
|
|
61
|
+
if (params.addDirs && params.addDirs.length > 0) {
|
|
62
|
+
for (const dir of params.addDirs) {
|
|
63
|
+
args.push('--add-dir', dir);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (useStdin) {
|
|
67
|
+
args.push('--input-format', 'stream-json');
|
|
68
|
+
}
|
|
69
|
+
if (effectiveOutputFormat) {
|
|
70
|
+
args.push('--output-format', effectiveOutputFormat);
|
|
71
|
+
}
|
|
72
|
+
if (effectiveOutputFormat === 'stream-json') {
|
|
73
|
+
args.push('--include-partial-messages');
|
|
74
|
+
}
|
|
75
|
+
if (params.tools) {
|
|
76
|
+
if (params.tools.length > 0) {
|
|
77
|
+
args.push('--tools', params.tools.join(','));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
args.push('--tools=');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!useStdin) {
|
|
84
|
+
args.push('--', params.prompt);
|
|
85
|
+
}
|
|
86
|
+
return args;
|
|
87
|
+
},
|
|
88
|
+
buildStdinPayload(ctx) {
|
|
89
|
+
if (!ctx.useStdin)
|
|
90
|
+
return null;
|
|
91
|
+
const { params } = ctx;
|
|
92
|
+
const content = [
|
|
93
|
+
{ type: 'text', text: params.prompt },
|
|
94
|
+
];
|
|
95
|
+
if (ctx.hasImages && params.images) {
|
|
96
|
+
for (const img of params.images) {
|
|
97
|
+
content.push({
|
|
98
|
+
type: 'image',
|
|
99
|
+
source: { type: 'base64', media_type: img.mediaType, data: img.base64 },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return JSON.stringify({ type: 'user', message: { role: 'user', content } }) + '\n';
|
|
104
|
+
},
|
|
105
|
+
parseLine(evt, ctx) {
|
|
106
|
+
if (!evt || typeof evt !== 'object')
|
|
107
|
+
return null;
|
|
108
|
+
const obj = evt;
|
|
109
|
+
// --- stream_event wrapper (Anthropic API events) ---
|
|
110
|
+
if (obj.type === 'stream_event' && obj.event && typeof obj.event === 'object') {
|
|
111
|
+
const inner = obj.event;
|
|
112
|
+
const idx = typeof inner.index === 'number' ? inner.index : -1;
|
|
113
|
+
// content_block_start — detect tool_use blocks for activity labels.
|
|
114
|
+
if (inner.type === 'content_block_start' && inner.content_block && typeof inner.content_block === 'object') {
|
|
115
|
+
const cb = inner.content_block;
|
|
116
|
+
if (cb.type === 'tool_use' && typeof cb.name === 'string') {
|
|
117
|
+
const ts = getToolState(ctx);
|
|
118
|
+
ts.activeTools.set(idx, cb.name);
|
|
119
|
+
ts.inputBufs.set(idx, '');
|
|
120
|
+
}
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
// content_block_delta — text, thinking, tool input, etc.
|
|
124
|
+
if (inner.type === 'content_block_delta' && inner.delta && typeof inner.delta === 'object') {
|
|
125
|
+
const delta = inner.delta;
|
|
126
|
+
if (delta.type === 'text_delta' && typeof delta.text === 'string') {
|
|
127
|
+
return { text: delta.text };
|
|
128
|
+
}
|
|
129
|
+
// Accumulate input_json_delta for tool input (used by tool_end to provide file paths).
|
|
130
|
+
if (delta.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
|
|
131
|
+
const ts = getToolState(ctx);
|
|
132
|
+
const buf = ts.inputBufs.get(idx);
|
|
133
|
+
if (buf !== undefined)
|
|
134
|
+
ts.inputBufs.set(idx, buf + delta.partial_json);
|
|
135
|
+
// Signal activity so the progress stall timer resets — tool input
|
|
136
|
+
// generation is real work, not a thinking spiral.
|
|
137
|
+
return { activity: true };
|
|
138
|
+
}
|
|
139
|
+
// thinking_delta, signature_delta, etc. — consumed, no text, no activity signal.
|
|
140
|
+
// thinking_delta intentionally does NOT set activity: the spiral detector
|
|
141
|
+
// is specifically designed to catch long thinking spans with no output.
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
// content_block_stop — emit tool_start (with parsed input) + tool_end for activity labels.
|
|
145
|
+
if (inner.type === 'content_block_stop') {
|
|
146
|
+
const ts = getToolState(ctx);
|
|
147
|
+
const name = ts.activeTools.get(idx);
|
|
148
|
+
if (name) {
|
|
149
|
+
let input;
|
|
150
|
+
const buf = ts.inputBufs.get(idx);
|
|
151
|
+
if (buf) {
|
|
152
|
+
try {
|
|
153
|
+
input = JSON.parse(buf);
|
|
154
|
+
}
|
|
155
|
+
catch { /* partial */ }
|
|
156
|
+
}
|
|
157
|
+
ts.activeTools.delete(idx);
|
|
158
|
+
ts.inputBufs.delete(idx);
|
|
159
|
+
// Emit tool_start with the full input (so toolActivityLabel gets file paths),
|
|
160
|
+
// then tool_end to transition back. The ToolAwareQueue handles this sequence.
|
|
161
|
+
const events = [
|
|
162
|
+
{ type: 'tool_start', name, ...(input ? { input } : {}) },
|
|
163
|
+
{ type: 'tool_end', name, ok: true },
|
|
164
|
+
];
|
|
165
|
+
return { extraEvents: events };
|
|
166
|
+
}
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
// All other stream_event types (message_start, message_delta, message_stop) — consumed, no text.
|
|
170
|
+
return {};
|
|
171
|
+
}
|
|
172
|
+
// --- assistant partial messages (from --include-partial-messages) ---
|
|
173
|
+
if (obj.type === 'assistant')
|
|
174
|
+
return {};
|
|
175
|
+
// --- result event ---
|
|
176
|
+
if (obj.type === 'result') {
|
|
177
|
+
const rt = extractResultText(evt);
|
|
178
|
+
const blocks = extractResultContentBlocks(evt);
|
|
179
|
+
if (rt || blocks) {
|
|
180
|
+
return {
|
|
181
|
+
resultText: rt ?? blocks?.text ?? null,
|
|
182
|
+
resultImages: blocks?.images,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
// --- system init event ---
|
|
188
|
+
if (obj.type === 'system')
|
|
189
|
+
return {};
|
|
190
|
+
// Unknown event — fall through to default parsing.
|
|
191
|
+
return null;
|
|
192
|
+
},
|
|
193
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Codex CLI adapter strategy.
|
|
2
|
+
// Provides model-specific logic for the universal CLI adapter factory.
|
|
3
|
+
/** Max chars for error messages exposed outside the adapter. Prevents prompt/session leaks. */
|
|
4
|
+
const MAX_ERROR_LENGTH = 200;
|
|
5
|
+
const CODEX_NOISY_LINE_PATTERNS = [
|
|
6
|
+
/^warning:/i,
|
|
7
|
+
/^openai codex v/i,
|
|
8
|
+
/^-+$/,
|
|
9
|
+
/^(workdir|model|provider|approval|sandbox|reasoning effort|reasoning summaries|session id):/i,
|
|
10
|
+
/^user$/i,
|
|
11
|
+
/^mcp startup:/i,
|
|
12
|
+
/^reconnecting\.\.\./i,
|
|
13
|
+
];
|
|
14
|
+
const CODEX_DIAGNOSTIC_LINE_PATTERN = /\berror\b|\bfailed\b|timed out|timeout|not found|permission denied|invalid|denied|unauthorized|forbidden|expired|rate limit|disconnected/i;
|
|
15
|
+
function isNoisyCodexLine(line) {
|
|
16
|
+
return CODEX_NOISY_LINE_PATTERNS.some((re) => re.test(line));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Strip prompt content and internal details from error messages.
|
|
20
|
+
* Codex CLI can include the full prompt, session paths, and auth details in stderr on failure.
|
|
21
|
+
*/
|
|
22
|
+
function sanitizeCodexError(raw) {
|
|
23
|
+
if (!raw)
|
|
24
|
+
return 'codex failed (no details)';
|
|
25
|
+
const lines = raw
|
|
26
|
+
.split('\n')
|
|
27
|
+
.map((l) => l.trim())
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
if (lines.length === 0)
|
|
30
|
+
return 'codex failed';
|
|
31
|
+
// Known Codex state DB corruption mode: stale/missing rollout paths in session index.
|
|
32
|
+
if (lines.some((l) => /state db (missing|returned stale) rollout path/i.test(l))) {
|
|
33
|
+
return 'codex session state appears corrupted (rollout path missing). Set CODEX_HOME to a clean directory and retry.'
|
|
34
|
+
.slice(0, MAX_ERROR_LENGTH);
|
|
35
|
+
}
|
|
36
|
+
const meaningful = lines.filter((l) => !isNoisyCodexLine(l));
|
|
37
|
+
const diagnostic = [...meaningful].reverse().find((l) => CODEX_DIAGNOSTIC_LINE_PATTERN.test(l));
|
|
38
|
+
if (diagnostic)
|
|
39
|
+
return diagnostic.slice(0, MAX_ERROR_LENGTH);
|
|
40
|
+
// Avoid leaking prompt content when stderr only contains non-diagnostic chatter.
|
|
41
|
+
return 'codex failed (no details)';
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create a Codex CLI adapter strategy.
|
|
45
|
+
* Factory function because defaultModel varies per runtime instance.
|
|
46
|
+
*/
|
|
47
|
+
export function createCodexStrategy(defaultModel) {
|
|
48
|
+
return {
|
|
49
|
+
id: 'codex',
|
|
50
|
+
binaryDefault: 'codex',
|
|
51
|
+
defaultModel,
|
|
52
|
+
capabilities: [
|
|
53
|
+
'streaming_text',
|
|
54
|
+
'tools_fs',
|
|
55
|
+
'tools_exec',
|
|
56
|
+
'tools_web',
|
|
57
|
+
'sessions',
|
|
58
|
+
],
|
|
59
|
+
multiTurnMode: 'session-resume',
|
|
60
|
+
getOutputMode(ctx, _opts) {
|
|
61
|
+
const wantSession = ctx.sessionMap != null && Boolean(ctx.params.sessionKey);
|
|
62
|
+
return wantSession ? 'jsonl' : 'text';
|
|
63
|
+
},
|
|
64
|
+
buildArgs(ctx, opts) {
|
|
65
|
+
const { params, useStdin } = ctx;
|
|
66
|
+
const wantSession = ctx.sessionMap != null && Boolean(params.sessionKey);
|
|
67
|
+
const existingThreadId = params.sessionKey ? ctx.sessionMap?.get(params.sessionKey) : undefined;
|
|
68
|
+
const dangerousBypass = Boolean(opts.dangerouslySkipPermissions);
|
|
69
|
+
// When resuming, use `codex exec resume <thread_id>`.
|
|
70
|
+
// The resume subcommand does NOT support -s/--sandbox (inherits from original session).
|
|
71
|
+
// When starting a new session (or ephemeral), use `codex exec`.
|
|
72
|
+
const args = existingThreadId
|
|
73
|
+
? ['exec', 'resume', existingThreadId, '-m', params.model, '--skip-git-repo-check']
|
|
74
|
+
: ['exec', '-m', params.model, '--skip-git-repo-check', ...(wantSession ? [] : ['--ephemeral'])];
|
|
75
|
+
if (dangerousBypass) {
|
|
76
|
+
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
77
|
+
}
|
|
78
|
+
else if (!existingThreadId) {
|
|
79
|
+
args.push('-s', 'read-only');
|
|
80
|
+
}
|
|
81
|
+
// When session tracking is active, use --json so we can capture the thread_id
|
|
82
|
+
// from the `thread.started` event.
|
|
83
|
+
if (wantSession) {
|
|
84
|
+
args.push('--json');
|
|
85
|
+
}
|
|
86
|
+
// Pass --add-dir flags for additional directories.
|
|
87
|
+
// The resume subcommand does NOT support --add-dir (inherits from original session).
|
|
88
|
+
if (!existingThreadId && params.addDirs && params.addDirs.length > 0) {
|
|
89
|
+
for (const dir of params.addDirs) {
|
|
90
|
+
args.push('--add-dir', dir);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// `--` terminates option parsing so prompts like "--- SOUL.md ---" are
|
|
94
|
+
// always treated as positional input rather than CLI flags.
|
|
95
|
+
args.push('--');
|
|
96
|
+
if (useStdin) {
|
|
97
|
+
// Use `-` to signal stdin reading.
|
|
98
|
+
args.push('-');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
args.push(params.prompt);
|
|
102
|
+
}
|
|
103
|
+
return args;
|
|
104
|
+
},
|
|
105
|
+
buildStdinPayload(ctx) {
|
|
106
|
+
if (!ctx.useStdin)
|
|
107
|
+
return null;
|
|
108
|
+
// Codex uses raw text stdin (not JSON-wrapped like Claude).
|
|
109
|
+
return ctx.params.prompt;
|
|
110
|
+
},
|
|
111
|
+
parseLine(evt, ctx) {
|
|
112
|
+
const anyEvt = evt;
|
|
113
|
+
// Capture thread_id for session resume on subsequent calls.
|
|
114
|
+
if (anyEvt.type === 'thread.started' && anyEvt.thread_id && ctx.params.sessionKey && ctx.sessionMap) {
|
|
115
|
+
ctx.sessionMap.set(ctx.params.sessionKey, String(anyEvt.thread_id));
|
|
116
|
+
return {}; // Handled — no text to emit.
|
|
117
|
+
}
|
|
118
|
+
// Extract text from completed items.
|
|
119
|
+
if (anyEvt.type === 'item.completed') {
|
|
120
|
+
const item = anyEvt.item;
|
|
121
|
+
if (!item)
|
|
122
|
+
return null;
|
|
123
|
+
// Reasoning items: stream as text_delta for the preview, but do not set
|
|
124
|
+
// resultText so the final reply remains answer-only.
|
|
125
|
+
if (item.type === 'reasoning') {
|
|
126
|
+
const summary = item.summary;
|
|
127
|
+
const text = item.text;
|
|
128
|
+
const reasoningText = typeof summary === 'string' ? summary :
|
|
129
|
+
typeof text === 'string' ? text :
|
|
130
|
+
null;
|
|
131
|
+
if (reasoningText)
|
|
132
|
+
return { text: reasoningText };
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
// Agent message: stream as text_delta and lock in resultText so that
|
|
136
|
+
// text_final uses the answer only and never falls back to merged
|
|
137
|
+
// (which now includes reasoning text).
|
|
138
|
+
if (item.type === 'agent_message' && typeof item.text === 'string') {
|
|
139
|
+
return { text: item.text, resultText: item.text };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Other JSONL events (turn.completed, etc.) — not handled by Codex strategy.
|
|
143
|
+
return null;
|
|
144
|
+
},
|
|
145
|
+
sanitizeError(raw) {
|
|
146
|
+
return sanitizeCodexError(raw);
|
|
147
|
+
},
|
|
148
|
+
handleSpawnError(err, binary) {
|
|
149
|
+
// Let the universal adapter handle timeouts.
|
|
150
|
+
if (err?.timedOut)
|
|
151
|
+
return null;
|
|
152
|
+
// Use fixed messages to prevent prompt/session leaks.
|
|
153
|
+
// execa's shortMessage/originalMessage can contain the full command line.
|
|
154
|
+
const code = err?.code || err?.errno || '';
|
|
155
|
+
const isNotFound = code === 'ENOENT' || String(err?.originalMessage || '').includes('ENOENT');
|
|
156
|
+
if (isNotFound)
|
|
157
|
+
return `codex binary not found (${binary}). Check CODEX_BIN or PATH.`;
|
|
158
|
+
return `codex process failed unexpectedly${code ? ` (${code})` : ''}`;
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Gemini CLI adapter strategy.
|
|
2
|
+
// Phase 1: one-shot text output, no sessions.
|
|
3
|
+
// Provides model-specific logic for the universal CLI adapter factory.
|
|
4
|
+
/** Max chars for error messages exposed outside the adapter. Prevents prompt leaks. */
|
|
5
|
+
const MAX_ERROR_LENGTH = 200;
|
|
6
|
+
function sanitizeGeminiError(raw) {
|
|
7
|
+
if (!raw)
|
|
8
|
+
return 'gemini failed (no details)';
|
|
9
|
+
const lines = raw
|
|
10
|
+
.split('\n')
|
|
11
|
+
.map((l) => l.trim())
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
if (lines.length === 0)
|
|
14
|
+
return 'gemini failed';
|
|
15
|
+
// Return only the first meaningful line to avoid leaking prompt content.
|
|
16
|
+
return lines[0].slice(0, MAX_ERROR_LENGTH);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create a Gemini CLI adapter strategy.
|
|
20
|
+
* Factory function because defaultModel varies per runtime instance.
|
|
21
|
+
*/
|
|
22
|
+
export function createGeminiStrategy(defaultModel) {
|
|
23
|
+
return {
|
|
24
|
+
id: 'gemini',
|
|
25
|
+
binaryDefault: 'gemini',
|
|
26
|
+
defaultModel,
|
|
27
|
+
capabilities: ['streaming_text'],
|
|
28
|
+
getOutputMode(_ctx, _opts) {
|
|
29
|
+
return 'text';
|
|
30
|
+
},
|
|
31
|
+
buildArgs(ctx, _opts) {
|
|
32
|
+
const { params, useStdin } = ctx;
|
|
33
|
+
const args = ['--model', params.model];
|
|
34
|
+
if (!useStdin) {
|
|
35
|
+
// `--` terminates option parsing so prompts like "--- SOUL.md ---" are
|
|
36
|
+
// always treated as positional input rather than CLI flags.
|
|
37
|
+
args.push('--', params.prompt);
|
|
38
|
+
}
|
|
39
|
+
// When useStdin is true, no positional arg is added — the binary reads from stdin.
|
|
40
|
+
return args;
|
|
41
|
+
},
|
|
42
|
+
buildStdinPayload(ctx) {
|
|
43
|
+
if (!ctx.useStdin)
|
|
44
|
+
return null;
|
|
45
|
+
// Gemini CLI reads raw text from stdin when no positional prompt is given.
|
|
46
|
+
return ctx.params.prompt;
|
|
47
|
+
},
|
|
48
|
+
sanitizeError(raw) {
|
|
49
|
+
return sanitizeGeminiError(raw);
|
|
50
|
+
},
|
|
51
|
+
handleSpawnError(err, binary) {
|
|
52
|
+
// Let the universal adapter handle timeouts.
|
|
53
|
+
if (err?.timedOut)
|
|
54
|
+
return null;
|
|
55
|
+
// Use fixed messages to prevent prompt leaks.
|
|
56
|
+
// execa's shortMessage/originalMessage can contain the full command line.
|
|
57
|
+
const code = err?.code || err?.errno || '';
|
|
58
|
+
const isNotFound = code === 'ENOENT' || String(err?.originalMessage || '').includes('ENOENT');
|
|
59
|
+
if (isNotFound)
|
|
60
|
+
return `gemini binary not found (${binary}). Check GEMINI_BIN or PATH.`;
|
|
61
|
+
return `gemini process failed unexpectedly${code ? ` (${code})` : ''}`;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|