@cohortapp/agent-sdk 2.17.0 → 2.18.4
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/.claude/settings.json +18 -0
- package/.env.example +18 -5
- package/README.md +1 -0
- package/bin/maestro.mjs +62 -0
- package/docs/guides/billing-console-keys.md +60 -0
- package/docs/guides/front-door-session.md +54 -9
- package/docs/guides/mac-mini.md +20 -25
- package/docs/guides/setup-wizard.md +1 -1
- package/docs/runbooks/fleet-rollout.md +156 -0
- package/docs/runbooks/mac-mini-bootstrap.md +12 -14
- package/lib/action-executor.js +19 -3
- package/lib/budget-guard.mjs +279 -3
- package/lib/channels/base-adapter.mjs +3 -1
- package/lib/channels/contract.mjs +2 -1
- package/lib/channels/inbox-item.mjs +8 -0
- package/lib/claude-bin.mjs +5 -6
- package/lib/cli/doctor-checks.mjs +141 -10
- package/lib/cli/global-setup-extras.mjs +5 -1
- package/lib/cli/inbox.mjs +100 -15
- package/lib/cli/seat-auth.mjs +463 -0
- package/lib/cli/session.mjs +80 -12
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/presence.mjs +142 -5
- package/lib/comms/send-gate.mjs +559 -1
- package/lib/diagnostics/alerts.mjs +49 -0
- package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
- package/lib/engine/agents/definitions.mjs +343 -0
- package/lib/engine/agents/persist.mjs +275 -0
- package/lib/engine/agents/runtime.mjs +748 -0
- package/lib/engine/agents/usage.mjs +95 -0
- package/lib/engine/auth-status.mjs +139 -0
- package/lib/engine/budget.mjs +194 -0
- package/lib/engine/cli.mjs +1204 -0
- package/lib/engine/commands/index.mjs +269 -0
- package/lib/engine/context/budget.mjs +219 -0
- package/lib/engine/context/cache.mjs +125 -0
- package/lib/engine/context/child-env.mjs +215 -0
- package/lib/engine/context/compaction.mjs +342 -0
- package/lib/engine/context/images.mjs +90 -0
- package/lib/engine/context/instructions.mjs +327 -0
- package/lib/engine/context/lazy-instructions.mjs +169 -0
- package/lib/engine/context/manager.mjs +182 -0
- package/lib/engine/context/real-path.mjs +91 -0
- package/lib/engine/context/secret-values.mjs +163 -0
- package/lib/engine/context/settings.mjs +274 -0
- package/lib/engine/context/stream-input.mjs +159 -0
- package/lib/engine/guard.mjs +152 -0
- package/lib/engine/hooks.mjs +713 -0
- package/lib/engine/loop.mjs +560 -0
- package/lib/engine/mcp/client.mjs +254 -0
- package/lib/engine/mcp/config.mjs +301 -0
- package/lib/engine/mcp/http.mjs +201 -0
- package/lib/engine/mcp/index.mjs +146 -0
- package/lib/engine/mcp/jsonrpc.mjs +147 -0
- package/lib/engine/mcp/naming.mjs +66 -0
- package/lib/engine/mcp/resources.mjs +89 -0
- package/lib/engine/mcp/results.mjs +133 -0
- package/lib/engine/mcp/stdio.mjs +137 -0
- package/lib/engine/mcp/supervisor.mjs +116 -0
- package/lib/engine/messages.mjs +104 -0
- package/lib/engine/output/json.mjs +164 -0
- package/lib/engine/output/stream-json.mjs +266 -0
- package/lib/engine/permissions.mjs +845 -0
- package/lib/engine/process-identity.mjs +164 -0
- package/lib/engine/process-tree.mjs +551 -0
- package/lib/engine/prompt.mjs +60 -0
- package/lib/engine/session/store.mjs +299 -0
- package/lib/engine/session-runtime/args.mjs +97 -0
- package/lib/engine/session-runtime/host.mjs +143 -0
- package/lib/engine/session-runtime/inbox.mjs +122 -0
- package/lib/engine/session-runtime/notifications.mjs +129 -0
- package/lib/engine/session-runtime/registry.mjs +328 -0
- package/lib/engine/session-runtime/runner.mjs +344 -0
- package/lib/engine/session-runtime/socket.mjs +212 -0
- package/lib/engine/session-runtime/wakeup.mjs +115 -0
- package/lib/engine/skills/index.mjs +321 -0
- package/lib/engine/tools/bash-background.mjs +533 -0
- package/lib/engine/tools/bash.mjs +216 -0
- package/lib/engine/tools/edit.mjs +97 -0
- package/lib/engine/tools/glob.mjs +81 -0
- package/lib/engine/tools/grep.mjs +224 -0
- package/lib/engine/tools/index.mjs +84 -0
- package/lib/engine/tools/list-agents.mjs +32 -0
- package/lib/engine/tools/ls.mjs +127 -0
- package/lib/engine/tools/monitor.mjs +82 -0
- package/lib/engine/tools/notebook-edit.mjs +218 -0
- package/lib/engine/tools/read.mjs +103 -0
- package/lib/engine/tools/schedule-wakeup.mjs +45 -0
- package/lib/engine/tools/schema.mjs +144 -0
- package/lib/engine/tools/send-message.mjs +77 -0
- package/lib/engine/tools/session.mjs +70 -0
- package/lib/engine/tools/todo.mjs +144 -0
- package/lib/engine/tools/toolsearch.mjs +217 -0
- package/lib/engine/tools/walk.mjs +193 -0
- package/lib/engine/tools/web-switch.mjs +31 -0
- package/lib/engine/tools/webfetch-html.mjs +387 -0
- package/lib/engine/tools/webfetch-net.mjs +340 -0
- package/lib/engine/tools/webfetch.mjs +198 -0
- package/lib/engine/tools/websearch.mjs +91 -0
- package/lib/engine/tools/workflow.mjs +95 -0
- package/lib/engine/tools/write.mjs +76 -0
- package/lib/engine/tui/line-editor.mjs +137 -0
- package/lib/engine/tui/render.mjs +86 -0
- package/lib/engine/tui/tui.mjs +274 -0
- package/lib/engine/wire/anthropic-messages.mjs +263 -0
- package/lib/engine/wire/effort.mjs +36 -0
- package/lib/engine/wire/errors.mjs +496 -0
- package/lib/engine/wire/http.mjs +441 -0
- package/lib/engine/wire/index.mjs +76 -0
- package/lib/engine/wire/openai-chat.mjs +332 -0
- package/lib/engine/wire/prompt-cache.mjs +79 -0
- package/lib/engine/wire/search.mjs +140 -0
- package/lib/engine/wire/sse.mjs +114 -0
- package/lib/engine/wire/stall.mjs +349 -0
- package/lib/engine/wire/token-provider.mjs +175 -0
- package/lib/engine/wire/usage.mjs +192 -0
- package/lib/engine/workflow/host.mjs +524 -0
- package/lib/engine/workflow/journal.mjs +188 -0
- package/lib/engine/workflow/json-schema.mjs +171 -0
- package/lib/engine/workflow/meta.mjs +329 -0
- package/lib/engine/workflow/notifications.mjs +52 -0
- package/lib/engine/workflow/runtime.mjs +447 -0
- package/lib/engine/workflow/sandbox.mjs +534 -0
- package/lib/engine/workflow/worker.mjs +141 -0
- package/lib/engine/workflow/worktree.mjs +74 -0
- package/lib/execution/disposition.mjs +1 -1
- package/lib/execution/intake.mjs +10 -0
- package/lib/execution/surface-policy.mjs +15 -0
- package/lib/learning/curator.mjs +8 -6
- package/lib/learning/reflect.mjs +8 -6
- package/lib/model-router/catalog/cohort.yaml +137 -0
- package/lib/model-router/catalog.mjs +118 -1
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +89 -3
- package/lib/model-router/spawn.mjs +46 -47
- package/lib/model-router/taxonomy.mjs +126 -4
- package/lib/org/cost-sync.mjs +141 -11
- package/lib/org/inbound/broadcast.mjs +289 -0
- package/lib/org/inbound/collective.mjs +375 -0
- package/lib/org/inbound/directedness.mjs +96 -8
- package/lib/org/inbound/facts.mjs +78 -2
- package/lib/org/inbound/project.mjs +22 -0
- package/lib/org/inbound/surfaces.mjs +14 -0
- package/lib/org/llm-token.mjs +879 -0
- package/lib/org/mesh.mjs +61 -0
- package/lib/org/messaging.mjs +3 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +15 -0
- package/lib/org/quota.mjs +520 -0
- package/lib/org/tool-surface.mjs +104 -16
- package/lib/org/ui-parity.mjs +16 -1
- package/lib/org/work-ledger.mjs +37 -6
- package/lib/rate-guard.mjs +114 -1
- package/lib/resource-governor.mjs +41 -6
- package/lib/runtime/adapter.mjs +823 -0
- package/lib/runtime/child-env.mjs +191 -0
- package/lib/runtime/legacy-shell-guard.mjs +97 -0
- package/lib/runtime/seat-engine.mjs +162 -0
- package/lib/session/ask-ledger.mjs +271 -0
- package/lib/session/current-work.mjs +676 -0
- package/lib/session/feed-core.mjs +40 -3
- package/lib/session/launch-args.mjs +56 -4
- package/lib/session/status-summary.mjs +26 -9
- package/lib/session/upgrade-notice.mjs +42 -0
- package/lib/setup/claude-probe.mjs +117 -13
- package/lib/setup/enrich.mjs +13 -10
- package/lib/setup/sections/model.mjs +39 -13
- package/lib/telemetry/collect.mjs +208 -9
- package/lib/upgrade/ignored-drift.mjs +105 -0
- package/lib/voice/post-call-brief.mjs +30 -17
- package/package.json +13 -3
- package/plugins/maestro-skills/skills/board-work.md +5 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
- package/plugins/maestro-skills/skills/main-session.md +18 -7
- package/scripts/ci/check-tarball-fidelity.mjs +126 -2
- package/scripts/ci/run-tests.mjs +47 -19
- package/scripts/cohort-llm/api-key-helper.mjs +92 -0
- package/scripts/collective/hook-runner.mjs +29 -2
- package/scripts/continuous-monitor.sh +13 -0
- package/scripts/cost/track-claude-usage.mjs +15 -0
- package/scripts/daemon/agent-daemon.mjs +408 -20
- package/scripts/daemon/assurance.mjs +48 -12
- package/scripts/daemon/cadence-consumer.mjs +218 -68
- package/scripts/daemon/cadence-handlers.mjs +73 -4
- package/scripts/daemon/classifier.mjs +75 -26
- package/scripts/daemon/context-compiler.mjs +51 -37
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +595 -149
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +24 -0
- package/scripts/daemon/responder.mjs +246 -79
- package/scripts/daemon/sdk-version.mjs +98 -16
- package/scripts/eval/probe-gateway.mjs +635 -0
- package/scripts/eval/replay/extract.mjs +270 -0
- package/scripts/eval/replay/grade.mjs +260 -0
- package/scripts/eval/replay/lib/config.mjs +50 -0
- package/scripts/eval/replay/lib/effects.mjs +65 -0
- package/scripts/eval/replay/lib/fixture.mjs +188 -0
- package/scripts/eval/replay/lib/judge.mjs +72 -0
- package/scripts/eval/replay/lib/redact.mjs +136 -0
- package/scripts/eval/replay/lib/sandbox.mjs +170 -0
- package/scripts/eval/replay/lib/schema-check.mjs +63 -0
- package/scripts/eval/replay/lib/transcript.mjs +76 -0
- package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
- package/scripts/eval/replay/report.mjs +185 -0
- package/scripts/eval/replay/run.mjs +404 -0
- package/scripts/fleet/rollout.mjs +1094 -0
- package/scripts/hooks/pre-send-audit.sh +36 -245
- package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
- package/scripts/hooks/validate-state-yaml.sh +190 -0
- package/scripts/huddle/huddle-llm.mjs +361 -0
- package/scripts/huddle/huddle-server.mjs +46 -121
- package/scripts/local-triggers/autoupdate.sh +448 -78
- package/scripts/local-triggers/run-trigger.sh +13 -0
- package/scripts/maintenance/pin-integrity.mjs +364 -0
- package/scripts/poll-slack-events.sh +41 -9
- package/scripts/poller/slack-socket-mode.mjs +28 -3
- package/scripts/session/supervisor.mjs +80 -13
- package/scripts/spawn-session.sh +13 -0
- package/bin/maestro.test.mjs +0 -1574
- package/lib/action-executor.test.mjs +0 -871
- package/lib/archetype.test.mjs +0 -132
- package/lib/assurance/plan-note.test.mjs +0 -234
- package/lib/assurance/room-budget.test.mjs +0 -486
- package/lib/assurance/tier.test.mjs +0 -174
- package/lib/autonomy.test.mjs +0 -66
- package/lib/backlog.test.mjs +0 -302
- package/lib/backup/policy.test.mjs +0 -305
- package/lib/budget-escalate.test.mjs +0 -232
- package/lib/budget-guard.envelope.test.mjs +0 -476
- package/lib/budget-guard.test.mjs +0 -427
- package/lib/cadence-bus-requeue.test.mjs +0 -83
- package/lib/cadence-bus-schedule.test.mjs +0 -194
- package/lib/cadence-bus.test.mjs +0 -720
- package/lib/cadences.test.mjs +0 -230
- package/lib/capability/inventory.test.mjs +0 -232
- package/lib/capability.test.mjs +0 -78
- package/lib/channels/base-adapter.test.mjs +0 -590
- package/lib/channels/channels.test.mjs +0 -371
- package/lib/channels/contract.test.mjs +0 -162
- package/lib/channels/inbox-item.test.mjs +0 -368
- package/lib/channels/orgmail/adapter.test.mjs +0 -448
- package/lib/channels/pairing.test.mjs +0 -270
- package/lib/channels/repeat-suppressor.test.mjs +0 -134
- package/lib/channels/slack-adapter.test.mjs +0 -212
- package/lib/channels/telegram-adapter.test.mjs +0 -306
- package/lib/channels/voice/adapter.test.mjs +0 -278
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
- package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
- package/lib/charter.test.mjs +0 -89
- package/lib/claude-bin.test.mjs +0 -131
- package/lib/cli/board.test.mjs +0 -227
- package/lib/cli/design.test.mjs +0 -270
- package/lib/cli/doctor-checks.test.mjs +0 -336
- package/lib/cli/global-setup-extras.test.mjs +0 -462
- package/lib/cli/inbox.test.mjs +0 -230
- package/lib/cli/session-ack.test.mjs +0 -63
- package/lib/cli/session.test.mjs +0 -613
- package/lib/collective/capture.test.mjs +0 -121
- package/lib/collective/cards.test.mjs +0 -114
- package/lib/collective/config.test.mjs +0 -123
- package/lib/collective/global-config.test.mjs +0 -220
- package/lib/collective/global-skills.test.mjs +0 -126
- package/lib/collective/presence.test.mjs +0 -95
- package/lib/collective/recall.test.mjs +0 -116
- package/lib/collective/vendor-skills.test.mjs +0 -306
- package/lib/comms/send-gate.test.mjs +0 -770
- package/lib/comms.test.mjs +0 -41
- package/lib/context/budget.test.mjs +0 -252
- package/lib/context/history-scope.test.mjs +0 -79
- package/lib/cost/ledger-row.test.mjs +0 -183
- package/lib/design/design-md.test.mjs +0 -318
- package/lib/design/fixtures/DESIGN.golden.md +0 -238
- package/lib/design/fixtures/PRODUCT.golden.md +0 -67
- package/lib/design/fixtures/foundation.json +0 -133
- package/lib/design/refresh-gate.test.mjs +0 -144
- package/lib/design/write.test.mjs +0 -241
- package/lib/diagnostics/alerts.test.mjs +0 -318
- package/lib/diagnostics/backup-freshness.test.mjs +0 -185
- package/lib/diagnostics/counters.test.mjs +0 -206
- package/lib/diagnostics/events.test.mjs +0 -290
- package/lib/diagnostics/otel.test.mjs +0 -196
- package/lib/diagnostics/trace.test.mjs +0 -251
- package/lib/env-compat.test.mjs +0 -104
- package/lib/execution/disposition.test.mjs +0 -553
- package/lib/execution/drive.test.mjs +0 -270
- package/lib/execution/effects.test.mjs +0 -344
- package/lib/execution/intake.test.mjs +0 -389
- package/lib/execution/journal.test.mjs +0 -261
- package/lib/execution/match.test.mjs +0 -235
- package/lib/execution/pipeline.test.mjs +0 -392
- package/lib/execution/route.test.mjs +0 -186
- package/lib/execution/surface-policy.test.mjs +0 -162
- package/lib/fs-atomic.test.mjs +0 -72
- package/lib/fs-ownership.test.mjs +0 -158
- package/lib/goals/admission.test.mjs +0 -164
- package/lib/goals/classify.test.mjs +0 -167
- package/lib/goals/collaborate.test.mjs +0 -336
- package/lib/goals/gaps.test.mjs +0 -284
- package/lib/goals/loop.test.mjs +0 -845
- package/lib/hooks/bus.test.mjs +0 -387
- package/lib/identity/persona.test.mjs +0 -142
- package/lib/kpi-sensors.test.mjs +0 -278
- package/lib/kpi.test.mjs +0 -244
- package/lib/learning/config.test.mjs +0 -75
- package/lib/learning/counters.test.mjs +0 -69
- package/lib/learning/curator-consolidate.test.mjs +0 -238
- package/lib/learning/curator.test.mjs +0 -106
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.test.mjs +0 -125
- package/lib/learning/skill-writer.test.mjs +0 -210
- package/lib/mandate/audit.test.mjs +0 -195
- package/lib/mandate/contract.test.mjs +0 -185
- package/lib/mandate/derive.test.mjs +0 -274
- package/lib/mandate/model.test.mjs +0 -164
- package/lib/mandate/refresh.test.mjs +0 -389
- package/lib/mcp/server.test.mjs +0 -426
- package/lib/model-router/auth-profiles.test.mjs +0 -580
- package/lib/model-router/catalog.test.mjs +0 -385
- package/lib/model-router/economics.test.mjs +0 -438
- package/lib/model-router/failover.test.mjs +0 -439
- package/lib/model-router/health.test.mjs +0 -338
- package/lib/model-router/integration-coverage.test.mjs +0 -831
- package/lib/model-router/integration.test.mjs +0 -564
- package/lib/model-router/ledger.test.mjs +0 -415
- package/lib/model-router/llm-task.test.mjs +0 -392
- package/lib/model-router/org-credentials.test.mjs +0 -265
- package/lib/model-router/pricing-refresh.test.mjs +0 -286
- package/lib/model-router/reconcile.test.mjs +0 -316
- package/lib/model-router/repair.test.mjs +0 -180
- package/lib/model-router/spawn.test.mjs +0 -446
- package/lib/model-router/taxonomy.test.mjs +0 -410
- package/lib/model-router.test.mjs +0 -1207
- package/lib/org/activity.test.mjs +0 -134
- package/lib/org/approvals.test.mjs +0 -216
- package/lib/org/awareness.test.mjs +0 -159
- package/lib/org/board-mine-cache.test.mjs +0 -53
- package/lib/org/board.test.mjs +0 -187
- package/lib/org/bootstrap-context.test.mjs +0 -153
- package/lib/org/client.test.mjs +0 -1206
- package/lib/org/cohort-client.test.mjs +0 -126
- package/lib/org/cost-sync.test.mjs +0 -153
- package/lib/org/doctor.test.mjs +0 -346
- package/lib/org/engagement-ledger.test.mjs +0 -112
- package/lib/org/engagement.test.mjs +0 -739
- package/lib/org/handoff.test.mjs +0 -269
- package/lib/org/inbound/directedness.test.mjs +0 -668
- package/lib/org/inbound/facts.test.mjs +0 -471
- package/lib/org/inbound/hydrate.test.mjs +0 -908
- package/lib/org/inbound/index.test.mjs +0 -429
- package/lib/org/inbound/project.test.mjs +0 -287
- package/lib/org/integration-tools.test.mjs +0 -160
- package/lib/org/keys.test.mjs +0 -92
- package/lib/org/knowledge.test.mjs +0 -326
- package/lib/org/leases.test.mjs +0 -235
- package/lib/org/mesh-directives.test.mjs +0 -110
- package/lib/org/mesh-integration.test.mjs +0 -127
- package/lib/org/mesh.test.mjs +0 -400
- package/lib/org/messaging.test.mjs +0 -471
- package/lib/org/param-contract.test.mjs +0 -477
- package/lib/org/policy.test.mjs +0 -237
- package/lib/org/protocol.checksum.test.mjs +0 -90
- package/lib/org/protocol.test.mjs +0 -323
- package/lib/org/push.test.mjs +0 -792
- package/lib/org/registry.test.mjs +0 -100
- package/lib/org/resource-tools.test.mjs +0 -361
- package/lib/org/tool-access.test.mjs +0 -144
- package/lib/org/tool-surface-integration.test.mjs +0 -120
- package/lib/org/tool-surface.test.mjs +0 -1268
- package/lib/org/typing.test.mjs +0 -291
- package/lib/org/ui-parity.test.mjs +0 -560
- package/lib/org/verify.test.mjs +0 -194
- package/lib/org/work-ledger.test.mjs +0 -273
- package/lib/plan/adoption-e2e.test.mjs +0 -366
- package/lib/plan/budget-enforcement.test.mjs +0 -400
- package/lib/plan/compile.test.mjs +0 -382
- package/lib/plan/emit.test.mjs +0 -269
- package/lib/plan/explain.test.mjs +0 -188
- package/lib/prompts/parallelism.test.mjs +0 -177
- package/lib/rag/rag.test.mjs +0 -505
- package/lib/rate-guard.test.mjs +0 -272
- package/lib/reactive-gate.test.mjs +0 -57
- package/lib/render.test.mjs +0 -68
- package/lib/resource-governor.test.mjs +0 -488
- package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
- package/lib/scheduling/jitter.test.mjs +0 -140
- package/lib/secrets/broker.test.mjs +0 -280
- package/lib/secrets/providers.test.mjs +0 -274
- package/lib/security/audit-engine.test.mjs +0 -424
- package/lib/security/coerce-args.test.mjs +0 -281
- package/lib/security/dangerous-tools.test.mjs +0 -68
- package/lib/security/external-content.test.mjs +0 -84
- package/lib/security/redact.test.mjs +0 -441
- package/lib/security/secret-equal.test.mjs +0 -55
- package/lib/session/config.test.mjs +0 -92
- package/lib/session/feed-core.test.mjs +0 -198
- package/lib/session/first-run.test.mjs +0 -121
- package/lib/session/frontdoor.test.mjs +0 -205
- package/lib/session/handoffs.test.mjs +0 -183
- package/lib/session/identity.test.mjs +0 -180
- package/lib/session/inbox-claims.test.mjs +0 -286
- package/lib/session/launch-args.test.mjs +0 -157
- package/lib/session/liveness.test.mjs +0 -100
- package/lib/session/status-summary.test.mjs +0 -118
- package/lib/session-permissions.test.mjs +0 -120
- package/lib/setup/claude-probe.test.mjs +0 -187
- package/lib/setup/completeness.test.mjs +0 -110
- package/lib/setup/context-pack.test.mjs +0 -89
- package/lib/setup/enrich.test.mjs +0 -115
- package/lib/setup/enroll-from-cohort.test.mjs +0 -300
- package/lib/setup/integration.test.mjs +0 -162
- package/lib/setup/io.test.mjs +0 -77
- package/lib/setup/runner.test.mjs +0 -132
- package/lib/setup/sections/identity.test.mjs +0 -234
- package/lib/setup/sections/inventory.test.mjs +0 -198
- package/lib/setup/sections/learning.test.mjs +0 -81
- package/lib/setup/sections/mandate.test.mjs +0 -388
- package/lib/setup/sections/messaging.test.mjs +0 -127
- package/lib/setup/sections/model.test.mjs +0 -240
- package/lib/setup/sections/org.test.mjs +0 -346
- package/lib/setup/sections/orgmail.test.mjs +0 -118
- package/lib/setup/sections/recovery.test.mjs +0 -98
- package/lib/setup/sections/subagents.test.mjs +0 -429
- package/lib/setup/sections/verify.test.mjs +0 -175
- package/lib/setup/sot.test.mjs +0 -81
- package/lib/setup/state.test.mjs +0 -115
- package/lib/singleton.test.mjs +0 -151
- package/lib/subagents/cli.test.mjs +0 -389
- package/lib/subagents/client.test.mjs +0 -309
- package/lib/subagents/gap.test.mjs +0 -234
- package/lib/subagents/lock.test.mjs +0 -248
- package/lib/subagents/manifest.test.mjs +0 -175
- package/lib/subagents/refs.test.mjs +0 -204
- package/lib/subagents/resolve.test.mjs +0 -422
- package/lib/subagents/schema.test.mjs +0 -328
- package/lib/telemetry/alerts.test.mjs +0 -109
- package/lib/telemetry/collect.test.mjs +0 -1274
- package/lib/tool-definitions-integration.test.mjs +0 -83
- package/lib/tool-definitions.test.mjs +0 -437
- package/lib/upgrade/global-refresh.test.mjs +0 -65
- package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
- package/lib/upgrade/post-steps.test.mjs +0 -200
- package/lib/upgrade/verify.test.mjs +0 -164
- package/lib/util/fetch-timeout.test.mjs +0 -202
- package/lib/util/reconnect.test.mjs +0 -369
- package/lib/util/unhandled.test.mjs +0 -216
- package/lib/voice/outbound.test.mjs +0 -69
- package/lib/voice/session-rotation.test.mjs +0 -114
- package/lib/voice/stt.test.mjs +0 -226
- package/lib/voice/voice.test.mjs +0 -990
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
- package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
- package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
- package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
- package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
- package/scripts/ci/check-skill-packs.test.mjs +0 -495
- package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
- package/scripts/ci/check.test.mjs +0 -194
- package/scripts/ci/conformance-org-api.test.mjs +0 -425
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
- package/scripts/collective/hook-runner.test.mjs +0 -173
- package/scripts/cost/fleet-digest.test.mjs +0 -207
- package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
- package/scripts/cost/track-claude-usage.test.mjs +0 -148
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
- package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
- package/scripts/daemon/agent-daemon.test.mjs +0 -995
- package/scripts/daemon/assurance-e2e.test.mjs +0 -613
- package/scripts/daemon/assurance.test.mjs +0 -1791
- package/scripts/daemon/board-mirror.test.mjs +0 -165
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
- package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
- package/scripts/daemon/cadence-consumer.test.mjs +0 -776
- package/scripts/daemon/cadence-handlers.test.mjs +0 -837
- package/scripts/daemon/classifier-identity.test.mjs +0 -137
- package/scripts/daemon/classifier.test.mjs +0 -266
- package/scripts/daemon/classify-kind.test.mjs +0 -40
- package/scripts/daemon/context-compiler.test.mjs +0 -406
- package/scripts/daemon/deliver.test.mjs +0 -564
- package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
- package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
- package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
- package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
- package/scripts/daemon/execution-ladder.test.mjs +0 -470
- package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
- package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
- package/scripts/daemon/inbox-deferral.test.mjs +0 -336
- package/scripts/daemon/inbox-wake.test.mjs +0 -199
- package/scripts/daemon/integration.test.mjs +0 -149
- package/scripts/daemon/lib/self-echo.test.mjs +0 -153
- package/scripts/daemon/lib/session-router.test.mjs +0 -554
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -556
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -221
- package/scripts/daemon/sdk-version.test.mjs +0 -31
- package/scripts/daemon/session-lock.test.mjs +0 -252
- package/scripts/daemon/session-outcomes.test.mjs +0 -533
- package/scripts/daemon/typing-registry.test.mjs +0 -102
- package/scripts/hooks/pre-send-audit.test.mjs +0 -354
- package/scripts/huddle/huddle-prompt.test.mjs +0 -176
- package/scripts/local-triggers/autoupdate.test.mjs +0 -518
- package/scripts/local-triggers/generate-plists.test.mjs +0 -456
- package/scripts/media-generation/brand-clause.test.mjs +0 -135
- package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
- package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
- package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
- package/scripts/poller/slack-socket-mode.test.mjs +0 -805
- package/scripts/poller-launchd/install.test.mjs +0 -243
- package/scripts/restore-from-backup.test.mjs +0 -181
- package/scripts/session/feed.test.mjs +0 -196
- package/scripts/session/supervisor-sh.test.mjs +0 -218
- package/scripts/session/supervisor.test.mjs +0 -482
- package/scripts/setup/configure-macos.test.mjs +0 -306
- package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
- package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
- package/scripts/setup/generate-capability.test.mjs +0 -134
- package/scripts/setup/init-agent.test.mjs +0 -370
- package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
- package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
- package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/tools/bash.mjs — the Bash tool.
|
|
3
|
+
*
|
|
4
|
+
* Input contract: `{ command, timeout?, description? }` (timeout in ms).
|
|
5
|
+
* Runs `bash -c <command>` in the run's working directory, non-interactively
|
|
6
|
+
* (stdin closed), in its own process group so a timeout or abort kills the
|
|
7
|
+
* whole tree, not just the shell.
|
|
8
|
+
*
|
|
9
|
+
* The child environment is SCRUBBED (`scrubToolEnv`) of anything shaped like a
|
|
10
|
+
* model or platform credential: every `*_API_KEY`, `*_AUTH_TOKEN`,
|
|
11
|
+
* `*_OAUTH_TOKEN`, and the COHORT_/NEOLITH_/MAESTRO_ tokens, including the
|
|
12
|
+
* engine's own gateway token. A command the model writes must never be able to
|
|
13
|
+
* read the seat token that pays for the model, or an org-leased provider key.
|
|
14
|
+
* Since W5-B (CF-22) the common secret shapes are removed too
|
|
15
|
+
* (`EXTENDED_SECRET_ENV`: GITHUB_TOKEN, AWS_SECRET_ACCESS_KEY, `*_SECRET`,
|
|
16
|
+
* `*_PASSWORD`, `*_PRIVATE_KEY`, …), unless the run's `envPassthrough` names
|
|
17
|
+
* them (`ctx.envPassthrough`). Since CF-118 a variable whose VALUE looks like a
|
|
18
|
+
* credential is removed too, whatever its name (`DATABASE_URL` with a password,
|
|
19
|
+
* a vendor token, a JWT, a long random or hex run: context/secret-values.mjs),
|
|
20
|
+
* under the same passthrough.
|
|
21
|
+
*
|
|
22
|
+
* Deferred to W2 (documented in docs/engine/README.md): background execution,
|
|
23
|
+
* a persistent working directory across calls, and permission rules. Until the
|
|
24
|
+
* permission engine lands, the caller decides whether Bash is exposed at all
|
|
25
|
+
* (`cohort run --tools`).
|
|
26
|
+
*
|
|
27
|
+
* @module lib/engine/tools/bash
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { spawn } from "node:child_process";
|
|
31
|
+
import { createProcessTracker, newProcessTag, PROCESS_TAG_ENV } from "../process-tree.mjs";
|
|
32
|
+
import { credentialValueKind } from "../context/secret-values.mjs";
|
|
33
|
+
|
|
34
|
+
export const DEFAULT_TIMEOUT_MS = 120_000;
|
|
35
|
+
export const MAX_TIMEOUT_MS = 600_000;
|
|
36
|
+
export const MAX_OUTPUT_CHARS = 30_000;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Credential SHAPES, not a list of names. Org provider keys are leased into the
|
|
40
|
+
* spawn env at startup and never written to disk, so this scrub is the only
|
|
41
|
+
* thing between a model-written command and them; an allow-list of names goes
|
|
42
|
+
* stale the day the model-router catalogue gains a provider (it already had:
|
|
43
|
+
* DEEPSEEK_, MOONSHOT_, DASHSCOPE_ were missed). `tools.test.mjs` pins every
|
|
44
|
+
* `auth_env` in `lib/model-router/catalog/*.yaml` against these patterns.
|
|
45
|
+
*/
|
|
46
|
+
export const SECRET_ENV = Object.freeze([
|
|
47
|
+
/(^|_)API_KEY$/i, // every provider key, incl. *_ADMIN_API_KEY
|
|
48
|
+
/(^|_)AUTH_TOKEN$/i, // ANTHROPIC_AUTH_TOKEN and kin
|
|
49
|
+
/(^|_)OAUTH_TOKEN$/i, // CLAUDE_CODE_OAUTH_TOKEN
|
|
50
|
+
/^(COHORT|NEOLITH|MAESTRO)_([A-Z0-9]+_)*TOKEN$/i, // COHORT_TOKEN, COHORT_API_TOKEN, COHORT_LLM_TOKEN, NEOLITH_*, MAESTRO_AGENT_TOKEN
|
|
51
|
+
/^DEEPINFRA_TOKEN$/i,
|
|
52
|
+
/^(COHORT|NEOLITH)_LLM_TOKEN_HELPER$/i, // the command that mints the engine's own gateway token
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* CF-22 (W5-B): common secret shapes beyond model and platform credentials —
|
|
57
|
+
* code-host and cloud tokens, `*_SECRET`, `*_PASSWORD`, `*_PRIVATE_KEY` and
|
|
58
|
+
* named bearer tokens. Scrubbed from the Bash tool, hook commands and MCP
|
|
59
|
+
* children like {@link SECRET_ENV}, except that an explicit passthrough list
|
|
60
|
+
* (settings `cohort.envPassthrough`, COHORT_ENGINE_ENV_PASSTHROUGH) may name
|
|
61
|
+
* one of THESE to let it through. A name that also matches SECRET_ENV is never
|
|
62
|
+
* passed by that list.
|
|
63
|
+
*/
|
|
64
|
+
export const EXTENDED_SECRET_ENV = Object.freeze([
|
|
65
|
+
/^(GITHUB|GH|GITLAB|NPM|HF|HUGGING_FACE_HUB|CIRCLE|VERCEL|NETLIFY|RAILWAY)_TOKEN$/i,
|
|
66
|
+
/^AWS_(SECRET_ACCESS_KEY|SESSION_TOKEN)$/i,
|
|
67
|
+
/(^|_)SECRET$/i, // CLIENT_SECRET, SLACK_SIGNING_SECRET, RELAY_SHARED_SECRET
|
|
68
|
+
/(^|_)SECRET_(ACCESS_)?KEY$/i,
|
|
69
|
+
/(^|_)(PASSWORD|PASSWD|PASSPHRASE)$/i,
|
|
70
|
+
/(^|_)PRIVATE_KEY$/i,
|
|
71
|
+
/(^|_)(ACCESS|REFRESH|SESSION|BOT|USER|APP|PERSONAL_ACCESS)_TOKEN$/i,
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
/** @param {string} name */
|
|
75
|
+
export function isExtendedSecretName(name) {
|
|
76
|
+
return EXTENDED_SECRET_ENV.some((re) => re.test(name));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The environment of a model-written command: no credential of either list,
|
|
81
|
+
* and no variable whose VALUE looks like a credential (CF-118,
|
|
82
|
+
* context/secret-values.mjs: a URL with a password, a vendor token, a JWT, a
|
|
83
|
+
* private key, a long random or hex run); `passthrough` names extended-shape or
|
|
84
|
+
* value-shaped variables to keep. A SECRET_ENV name is never kept.
|
|
85
|
+
* @param {NodeJS.ProcessEnv} env
|
|
86
|
+
* @param {{passthrough?:Iterable<string>}} [o]
|
|
87
|
+
* @returns {Record<string,string>}
|
|
88
|
+
*/
|
|
89
|
+
export function scrubToolEnv(env, { passthrough = [] } = {}) {
|
|
90
|
+
const pass = new Set(passthrough);
|
|
91
|
+
const out = {};
|
|
92
|
+
for (const [k, v] of Object.entries(env || {})) {
|
|
93
|
+
if (v === undefined) continue;
|
|
94
|
+
if (SECRET_ENV.some((re) => re.test(k))) continue;
|
|
95
|
+
if (!pass.has(k) && (isExtendedSecretName(k) || credentialValueKind(v, { name: k }) !== null)) continue;
|
|
96
|
+
out[k] = v;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Keep the head and tail of oversized output (errors usually live at the end).
|
|
103
|
+
* @param {string} s @param {number} [max]
|
|
104
|
+
*/
|
|
105
|
+
export function truncateOutput(s, max = MAX_OUTPUT_CHARS) {
|
|
106
|
+
if (s.length <= max) return s;
|
|
107
|
+
const half = Math.floor(max / 2);
|
|
108
|
+
return `${s.slice(0, half)}\n\n… [${s.length - max} characters omitted] …\n\n${s.slice(-half)}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const bashTool = {
|
|
112
|
+
name: "Bash",
|
|
113
|
+
readOnly: false,
|
|
114
|
+
description:
|
|
115
|
+
"Run a shell command with bash in the working directory and return its combined stdout and stderr. " +
|
|
116
|
+
"Commands run non-interactively with no stdin. " +
|
|
117
|
+
`\`timeout\` is in milliseconds (default ${DEFAULT_TIMEOUT_MS}, maximum ${MAX_TIMEOUT_MS}). ` +
|
|
118
|
+
"Give a short `description` of what the command does. Prefer Read, Glob, Grep and Edit over shell equivalents.",
|
|
119
|
+
input_schema: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {
|
|
122
|
+
command: { type: "string", description: "The command to run." },
|
|
123
|
+
timeout: { type: "integer", minimum: 1, maximum: MAX_TIMEOUT_MS, description: "Timeout in milliseconds." },
|
|
124
|
+
description: { type: "string", description: "What the command does, in a few words." },
|
|
125
|
+
},
|
|
126
|
+
required: ["command"],
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {{command:string, timeout?:number}} input
|
|
131
|
+
* @param {import('./index.mjs').ToolContext} ctx
|
|
132
|
+
*/
|
|
133
|
+
run(input, ctx) {
|
|
134
|
+
const timeoutMs = Math.min(input.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
if (ctx.signal?.aborted) return resolve({ ok: false, error: "Aborted before the command started." });
|
|
137
|
+
// CF-48: a timeout or abort also stops what the command started outside its group (setsid).
|
|
138
|
+
const tag = newProcessTag();
|
|
139
|
+
const rootId = `bash_fg_${tag}`;
|
|
140
|
+
// CF-119: sampled like a background shell (tree and tag, 100 ms, 500 ms, every 2 s, read without
|
|
141
|
+
// holding the event loop; a call that ends sooner costs nothing), and captured again at the kill.
|
|
142
|
+
const tracker = createProcessTracker();
|
|
143
|
+
let child;
|
|
144
|
+
try {
|
|
145
|
+
child = spawn("/bin/bash", ["-c", input.command], {
|
|
146
|
+
cwd: ctx.cwd,
|
|
147
|
+
env: { ...scrubToolEnv(ctx.env, { passthrough: ctx.envPassthrough ?? [] }), [PROCESS_TAG_ENV]: tag },
|
|
148
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
149
|
+
detached: true,
|
|
150
|
+
});
|
|
151
|
+
} catch (e) {
|
|
152
|
+
return resolve({ ok: false, error: `Could not start bash: ${e instanceof Error ? e.message : e}` });
|
|
153
|
+
}
|
|
154
|
+
if (child.pid) tracker.track({ id: rootId, pid: child.pid, tag });
|
|
155
|
+
let output = "";
|
|
156
|
+
let over = false;
|
|
157
|
+
const onData = (d) => {
|
|
158
|
+
if (output.length > MAX_OUTPUT_CHARS * 4) {
|
|
159
|
+
over = true;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
output += d.toString("utf8");
|
|
163
|
+
};
|
|
164
|
+
child.stdout.on("data", onData);
|
|
165
|
+
child.stderr.on("data", onData);
|
|
166
|
+
|
|
167
|
+
let reason = null;
|
|
168
|
+
const killTree = (why) => {
|
|
169
|
+
if (reason) return;
|
|
170
|
+
reason = why;
|
|
171
|
+
try {
|
|
172
|
+
tracker.capture([rootId], { tags: true });
|
|
173
|
+
tracker.signal([rootId], "SIGTERM");
|
|
174
|
+
} catch {
|
|
175
|
+
/* the group signal still goes out */
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
process.kill(-child.pid, "SIGTERM");
|
|
179
|
+
} catch {
|
|
180
|
+
/* the group already exited */
|
|
181
|
+
}
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
try {
|
|
184
|
+
process.kill(-child.pid, "SIGKILL");
|
|
185
|
+
} catch {
|
|
186
|
+
/* exited on SIGTERM, as hoped */
|
|
187
|
+
}
|
|
188
|
+
tracker.signal([rootId], "SIGKILL");
|
|
189
|
+
tracker.dispose();
|
|
190
|
+
}, 2000).unref();
|
|
191
|
+
};
|
|
192
|
+
const timer = setTimeout(() => killTree("timeout"), timeoutMs);
|
|
193
|
+
const onAbort = () => killTree("aborted");
|
|
194
|
+
ctx.signal?.addEventListener("abort", onAbort, { once: true });
|
|
195
|
+
|
|
196
|
+
const finish = (code, signal) => {
|
|
197
|
+
clearTimeout(timer);
|
|
198
|
+
ctx.signal?.removeEventListener("abort", onAbort);
|
|
199
|
+
tracker.rootExited(rootId);
|
|
200
|
+
if (!reason) tracker.dispose(); // a kill disposes after its SIGKILL pass
|
|
201
|
+
const body = truncateOutput(output) + (over ? "\n… [output limit reached; later output dropped]" : "");
|
|
202
|
+
if (reason === "timeout") return resolve({ ok: false, error: `${body}\nCommand timed out after ${timeoutMs} ms.`.trim() });
|
|
203
|
+
if (reason === "aborted") return resolve({ ok: false, error: `${body}\nCommand aborted.`.trim() });
|
|
204
|
+
if (code === 0) return resolve({ ok: true, content: body === "" ? "(no output)" : body });
|
|
205
|
+
const status = code === null ? `killed by ${signal}` : `exit code ${code}`;
|
|
206
|
+
resolve({ ok: false, error: `${body}\nCommand failed: ${status}.`.trim() });
|
|
207
|
+
};
|
|
208
|
+
child.on("error", (e) => {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
tracker.dispose();
|
|
211
|
+
resolve({ ok: false, error: `bash error: ${e.message}` });
|
|
212
|
+
});
|
|
213
|
+
child.on("close", finish);
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/tools/edit.mjs — the Edit tool.
|
|
3
|
+
*
|
|
4
|
+
* Input contract: `{ file_path, old_string, new_string, replace_all? }`.
|
|
5
|
+
* Exact string replacement. `old_string` must occur exactly once unless
|
|
6
|
+
* `replace_all` is set — an ambiguous edit is refused with the occurrence
|
|
7
|
+
* count, so the model widens the context instead of editing the wrong site.
|
|
8
|
+
* An empty `old_string` on a file that does not exist creates it.
|
|
9
|
+
*
|
|
10
|
+
* The replacement is a pure function (`applyEdit`); the tool is its edge.
|
|
11
|
+
*
|
|
12
|
+
* @module lib/engine/tools/edit
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, statSync } from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { writeFileAtomic } from "../../fs-atomic.mjs";
|
|
18
|
+
import { checkFresh } from "./write.mjs";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} text
|
|
22
|
+
* @param {string} oldString
|
|
23
|
+
* @param {string} newString
|
|
24
|
+
* @param {boolean} replaceAll
|
|
25
|
+
* @returns {{ ok:true, text:string, count:number } | { ok:false, error:string }}
|
|
26
|
+
*/
|
|
27
|
+
export function applyEdit(text, oldString, newString, replaceAll) {
|
|
28
|
+
if (oldString === newString) return { ok: false, error: "old_string and new_string are identical; nothing to change." };
|
|
29
|
+
if (oldString === "") return { ok: false, error: "old_string is empty; give the exact text to replace." };
|
|
30
|
+
const parts = text.split(oldString);
|
|
31
|
+
const count = parts.length - 1;
|
|
32
|
+
if (count === 0) return { ok: false, error: "old_string was not found in the file. Read the file and copy the text exactly." };
|
|
33
|
+
if (count > 1 && !replaceAll) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: `old_string occurs ${count} times. Include more surrounding text to make it unique, or set replace_all.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// split/join, not String.replace: a `$&` in new_string must stay literal.
|
|
40
|
+
return { ok: true, text: parts.join(newString), count };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const editTool = {
|
|
44
|
+
name: "Edit",
|
|
45
|
+
readOnly: false,
|
|
46
|
+
description:
|
|
47
|
+
"Replace an exact string in a file. `file_path` must be absolute and the file must have been Read first. " +
|
|
48
|
+
"`old_string` must match the file exactly (including whitespace) and occur once, unless `replace_all` is true. " +
|
|
49
|
+
"With an empty `old_string`, creates a new file containing `new_string`.",
|
|
50
|
+
input_schema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
file_path: { type: "string", description: "Absolute path of the file to change." },
|
|
54
|
+
old_string: { type: "string", description: "The exact text to replace." },
|
|
55
|
+
new_string: { type: "string", description: "The replacement text." },
|
|
56
|
+
replace_all: { type: "boolean", description: "Replace every occurrence instead of exactly one." },
|
|
57
|
+
},
|
|
58
|
+
required: ["file_path", "old_string", "new_string"],
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {{file_path:string, old_string:string, new_string:string, replace_all?:boolean}} input
|
|
63
|
+
* @param {import('./index.mjs').ToolContext} ctx
|
|
64
|
+
*/
|
|
65
|
+
async run(input, ctx) {
|
|
66
|
+
const file = input.file_path;
|
|
67
|
+
if (!path.isAbsolute(file)) return { ok: false, error: `file_path must be absolute, got "${file}"` };
|
|
68
|
+
let st = null;
|
|
69
|
+
try {
|
|
70
|
+
st = statSync(file);
|
|
71
|
+
} catch {
|
|
72
|
+
st = null; // absent: only the create form (empty old_string) is valid
|
|
73
|
+
}
|
|
74
|
+
if (!st) {
|
|
75
|
+
if (input.old_string !== "") return { ok: false, error: `File does not exist: ${file}` };
|
|
76
|
+
try {
|
|
77
|
+
writeFileAtomic(file, input.new_string);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
return { ok: false, error: `Could not create ${file}: ${e instanceof Error ? e.message : e}` };
|
|
80
|
+
}
|
|
81
|
+
ctx.fileState.set(file, { mtimeMs: statSync(file).mtimeMs });
|
|
82
|
+
return { ok: true, content: `Created ${file}.` };
|
|
83
|
+
}
|
|
84
|
+
if (st.isDirectory()) return { ok: false, error: `${file} is a directory.` };
|
|
85
|
+
const fresh = checkFresh(file, st, ctx.fileState);
|
|
86
|
+
if (!fresh.ok) return fresh;
|
|
87
|
+
const r = applyEdit(readFileSync(file, "utf8"), input.old_string, input.new_string, Boolean(input.replace_all));
|
|
88
|
+
if (!r.ok) return r;
|
|
89
|
+
try {
|
|
90
|
+
writeFileAtomic(file, r.text, { mode: st.mode & 0o7777 });
|
|
91
|
+
} catch (e) {
|
|
92
|
+
return { ok: false, error: `Could not write ${file}: ${e instanceof Error ? e.message : e}` };
|
|
93
|
+
}
|
|
94
|
+
ctx.fileState.set(file, { mtimeMs: statSync(file).mtimeMs });
|
|
95
|
+
return { ok: true, content: `Edited ${file} (${r.count} replacement${r.count === 1 ? "" : "s"}).` };
|
|
96
|
+
},
|
|
97
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/tools/glob.mjs — the Glob tool.
|
|
3
|
+
*
|
|
4
|
+
* Input contract: `{ pattern, path? }`. Returns matching file paths (absolute),
|
|
5
|
+
* newest first, capped. Candidate files come from ripgrep when available
|
|
6
|
+
* (honours .gitignore) or the pure-JS walk; matching is always `matchGlob`, so
|
|
7
|
+
* both paths agree on semantics.
|
|
8
|
+
*
|
|
9
|
+
* @module lib/engine/tools/glob
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { statSync } from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { hasRipgrep, runRipgrep, walkFiles, matchGlob, sortByMtime, toPosix, RG_EXCLUDES } from "./walk.mjs";
|
|
15
|
+
|
|
16
|
+
export const MAX_GLOB_RESULTS = 100;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the search root for Glob/Grep: absolute, or relative to cwd.
|
|
20
|
+
* @param {string|undefined} p @param {string} cwd
|
|
21
|
+
*/
|
|
22
|
+
export function resolveRoot(p, cwd) {
|
|
23
|
+
if (!p) return cwd;
|
|
24
|
+
return path.isAbsolute(p) ? p : path.resolve(cwd, p);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* List candidate files under root (absolute paths).
|
|
29
|
+
* @param {string} root
|
|
30
|
+
* @param {import('./index.mjs').ToolContext} ctx
|
|
31
|
+
* @returns {Promise<{ok:true, files:string[]} | {ok:false, error:string}>}
|
|
32
|
+
*/
|
|
33
|
+
export async function listFiles(root, ctx) {
|
|
34
|
+
if (ctx.useRipgrep ?? hasRipgrep(ctx.env)) {
|
|
35
|
+
const r = await runRipgrep(["--files", "--no-config", "--no-messages", ...RG_EXCLUDES, "--", root], { cwd: ctx.cwd, signal: ctx.signal });
|
|
36
|
+
if (r.ok) return { ok: true, files: r.stdout.split("\n").filter(Boolean) };
|
|
37
|
+
// ripgrep present but failing: the walk still answers.
|
|
38
|
+
}
|
|
39
|
+
return { ok: true, files: walkFiles(root, { signal: ctx.signal }) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const globTool = {
|
|
43
|
+
name: "Glob",
|
|
44
|
+
readOnly: true,
|
|
45
|
+
description:
|
|
46
|
+
"Find files by name pattern, such as `**/*.mjs` or `src/**/*.{ts,tsx}`. Returns absolute paths, most recently modified first. " +
|
|
47
|
+
"A pattern without a slash matches file names at any depth. `path` is the directory to search (default: the working directory). " +
|
|
48
|
+
"Use Grep to search file contents.",
|
|
49
|
+
input_schema: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
pattern: { type: "string", description: "Glob pattern to match file paths against." },
|
|
53
|
+
path: { type: "string", description: "Directory to search in. Defaults to the working directory." },
|
|
54
|
+
},
|
|
55
|
+
required: ["pattern"],
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {{pattern:string, path?:string}} input
|
|
60
|
+
* @param {import('./index.mjs').ToolContext} ctx
|
|
61
|
+
*/
|
|
62
|
+
async run(input, ctx) {
|
|
63
|
+
const root = resolveRoot(input.path, ctx.cwd);
|
|
64
|
+
try {
|
|
65
|
+
if (!statSync(root).isDirectory()) return { ok: false, error: `${root} is not a directory.` };
|
|
66
|
+
} catch {
|
|
67
|
+
return { ok: false, error: `Directory does not exist: ${root}` };
|
|
68
|
+
}
|
|
69
|
+
const listed = await listFiles(root, ctx);
|
|
70
|
+
if (!listed.ok) return listed;
|
|
71
|
+
const matches = listed.files.filter((f) => matchGlob(toPosix(path.relative(root, f)), input.pattern));
|
|
72
|
+
if (matches.length === 0) return { ok: true, content: "No files found." };
|
|
73
|
+
const sorted = sortByMtime(matches);
|
|
74
|
+
const shown = sorted.slice(0, MAX_GLOB_RESULTS);
|
|
75
|
+
const more = sorted.length - shown.length;
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
content: shown.join("\n") + (more > 0 ? `\n(${more} more results not shown; narrow the pattern or path)` : ""),
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/tools/grep.mjs — the Grep tool.
|
|
3
|
+
*
|
|
4
|
+
* Input contract: `{ pattern, path?, glob?, type?, output_mode?, "-i"?, "-n"?,
|
|
5
|
+
* "-A"?, "-B"?, "-C"?, multiline?, head_limit? }`. `output_mode` is
|
|
6
|
+
* `files_with_matches` (default: paths, newest first), `content` (matching
|
|
7
|
+
* lines, `path:line:text`) or `count` (`path:count`).
|
|
8
|
+
*
|
|
9
|
+
* Ripgrep when available; otherwise a pure-JS search with JavaScript regex
|
|
10
|
+
* syntax (close to, not identical to, ripgrep's). `glob` is always decided by
|
|
11
|
+
* `matchGlob` in both modes: ripgrep's own `--glob` anchors slash patterns
|
|
12
|
+
* differently (`/src/*.mjs` matches for rg, not for us), so in ripgrep mode
|
|
13
|
+
* the candidates come from `rg --files` and are filtered here before the
|
|
14
|
+
* search. `type` uses ripgrep's type table in ripgrep mode and
|
|
15
|
+
* `TYPE_EXTENSIONS` in the fallback.
|
|
16
|
+
*
|
|
17
|
+
* @module lib/engine/tools/grep
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync, statSync } from "node:fs";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { hasRipgrep, runRipgrep, walkFiles, matchGlob, sortByMtime, toPosix, RG_EXCLUDES } from "./walk.mjs";
|
|
23
|
+
import { resolveRoot } from "./glob.mjs";
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_HEAD_LIMIT = 250;
|
|
26
|
+
const MAX_SEARCH_BYTES = 2 * 1024 * 1024;
|
|
27
|
+
/** Explicit file arguments per ripgrep invocation, well under ARG_MAX. */
|
|
28
|
+
const RG_FILES_PER_CALL = 500;
|
|
29
|
+
|
|
30
|
+
/** A small file-type table for the fallback; ripgrep uses its own. */
|
|
31
|
+
export const TYPE_EXTENSIONS = {
|
|
32
|
+
js: [".js", ".mjs", ".cjs", ".jsx"],
|
|
33
|
+
ts: [".ts", ".tsx", ".mts", ".cts"],
|
|
34
|
+
py: [".py"],
|
|
35
|
+
go: [".go"],
|
|
36
|
+
rust: [".rs"],
|
|
37
|
+
java: [".java"],
|
|
38
|
+
md: [".md", ".markdown"],
|
|
39
|
+
json: [".json"],
|
|
40
|
+
yaml: [".yaml", ".yml"],
|
|
41
|
+
css: [".css", ".scss"],
|
|
42
|
+
html: [".html", ".htm"],
|
|
43
|
+
sh: [".sh", ".bash", ".zsh"],
|
|
44
|
+
sql: [".sql"],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Pure: search one file's text and render in the requested mode.
|
|
49
|
+
* @param {string} file display path
|
|
50
|
+
* @param {string} text
|
|
51
|
+
* @param {RegExp} re must carry the `g` flag
|
|
52
|
+
* @param {{ mode: 'content'|'count'|'files_with_matches', lineNumbers: boolean, before: number, after: number, multiline: boolean }} o
|
|
53
|
+
* @returns {string[]} output lines (empty when no match)
|
|
54
|
+
*/
|
|
55
|
+
export function searchText(file, text, re, o) {
|
|
56
|
+
if (o.multiline) {
|
|
57
|
+
re.lastIndex = 0;
|
|
58
|
+
const hits = [...text.matchAll(re)];
|
|
59
|
+
if (hits.length === 0) return [];
|
|
60
|
+
if (o.mode === "files_with_matches") return [file];
|
|
61
|
+
if (o.mode === "count") return [`${file}:${hits.length}`];
|
|
62
|
+
return hits.map((m) => {
|
|
63
|
+
const line = text.slice(0, m.index).split("\n").length;
|
|
64
|
+
return o.lineNumbers ? `${file}:${line}:${m[0]}` : `${file}:${m[0]}`;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const lines = text.split(/\r?\n/);
|
|
68
|
+
const matched = [];
|
|
69
|
+
const single = new RegExp(re.source, re.flags.replace("g", ""));
|
|
70
|
+
lines.forEach((l, i) => {
|
|
71
|
+
if (single.test(l)) matched.push(i);
|
|
72
|
+
});
|
|
73
|
+
if (matched.length === 0) return [];
|
|
74
|
+
if (o.mode === "files_with_matches") return [file];
|
|
75
|
+
if (o.mode === "count") return [`${file}:${matched.length}`];
|
|
76
|
+
const include = new Set();
|
|
77
|
+
for (const i of matched) for (let j = Math.max(0, i - o.before); j <= Math.min(lines.length - 1, i + o.after); j++) include.add(j);
|
|
78
|
+
const hit = new Set(matched);
|
|
79
|
+
const out = [];
|
|
80
|
+
let prev = -2;
|
|
81
|
+
for (const j of [...include].sort((a, b) => a - b)) {
|
|
82
|
+
if (prev >= 0 && j > prev + 1) out.push("--");
|
|
83
|
+
const sep = hit.has(j) ? ":" : "-";
|
|
84
|
+
out.push(o.lineNumbers ? `${file}${sep}${j + 1}${sep}${lines[j]}` : `${file}${sep}${lines[j]}`);
|
|
85
|
+
prev = j;
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const grepTool = {
|
|
91
|
+
name: "Grep",
|
|
92
|
+
readOnly: true,
|
|
93
|
+
description:
|
|
94
|
+
"Search file contents with a regular expression. `output_mode` is `files_with_matches` (default; file paths, newest first), " +
|
|
95
|
+
"`content` (matching lines as path:line:text) or `count` (matches per file). " +
|
|
96
|
+
"Narrow the search with `path`, `glob` (e.g. `*.mjs`) or `type` (e.g. `js`, `py`). " +
|
|
97
|
+
"`-i` ignores case; `-A`/`-B`/`-C` add context lines in content mode; `multiline` lets a pattern span lines; " +
|
|
98
|
+
`\`head_limit\` caps output lines (default ${DEFAULT_HEAD_LIMIT}).`,
|
|
99
|
+
input_schema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
pattern: { type: "string", description: "Regular expression to search for." },
|
|
103
|
+
path: { type: "string", description: "File or directory to search. Defaults to the working directory." },
|
|
104
|
+
glob: { type: "string", description: "Only search files whose names match this glob." },
|
|
105
|
+
type: { type: "string", description: "Only search files of this type (js, ts, py, go, rust, md, json, …)." },
|
|
106
|
+
output_mode: { type: "string", enum: ["content", "files_with_matches", "count"], description: "What to return." },
|
|
107
|
+
"-i": { type: "boolean", description: "Case-insensitive search." },
|
|
108
|
+
"-n": { type: "boolean", description: "Show line numbers in content mode (default true)." },
|
|
109
|
+
"-A": { type: "integer", minimum: 0, description: "Lines of context after each match (content mode)." },
|
|
110
|
+
"-B": { type: "integer", minimum: 0, description: "Lines of context before each match (content mode)." },
|
|
111
|
+
"-C": { type: "integer", minimum: 0, description: "Lines of context around each match (content mode)." },
|
|
112
|
+
multiline: { type: "boolean", description: "Allow the pattern to match across lines." },
|
|
113
|
+
head_limit: { type: "integer", minimum: 1, description: "Maximum number of output lines." },
|
|
114
|
+
},
|
|
115
|
+
required: ["pattern"],
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {any} input
|
|
120
|
+
* @param {import('./index.mjs').ToolContext} ctx
|
|
121
|
+
*/
|
|
122
|
+
async run(input, ctx) {
|
|
123
|
+
const root = resolveRoot(input.path, ctx.cwd);
|
|
124
|
+
let rootStat;
|
|
125
|
+
try {
|
|
126
|
+
rootStat = statSync(root);
|
|
127
|
+
} catch {
|
|
128
|
+
return { ok: false, error: `Path does not exist: ${root}` };
|
|
129
|
+
}
|
|
130
|
+
const mode = input.output_mode || "files_with_matches";
|
|
131
|
+
const o = {
|
|
132
|
+
mode,
|
|
133
|
+
lineNumbers: input["-n"] ?? true,
|
|
134
|
+
before: input["-B"] ?? input["-C"] ?? 0,
|
|
135
|
+
after: input["-A"] ?? input["-C"] ?? 0,
|
|
136
|
+
multiline: Boolean(input.multiline),
|
|
137
|
+
};
|
|
138
|
+
const headLimit = input.head_limit ?? DEFAULT_HEAD_LIMIT;
|
|
139
|
+
if (input.type && !TYPE_EXTENSIONS[input.type] && !(ctx.useRipgrep ?? hasRipgrep(ctx.env))) {
|
|
140
|
+
return { ok: false, error: `Unknown type "${input.type}". Known: ${Object.keys(TYPE_EXTENSIONS).join(", ")}.` };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Path a glob is matched against: relative to a directory root, else the basename. */
|
|
144
|
+
const globPath = (f) => toPosix(rootStat.isDirectory() ? path.relative(root, f) : path.basename(f));
|
|
145
|
+
|
|
146
|
+
let lines;
|
|
147
|
+
if (ctx.useRipgrep ?? hasRipgrep(ctx.env)) {
|
|
148
|
+
const args = ["--no-config", "--no-messages", "--color", "never", ...RG_EXCLUDES];
|
|
149
|
+
if (mode === "files_with_matches") args.push("-l");
|
|
150
|
+
else if (mode === "count") args.push("-c", "--with-filename");
|
|
151
|
+
else {
|
|
152
|
+
args.push("--with-filename", "--no-heading");
|
|
153
|
+
if (o.lineNumbers) args.push("-n");
|
|
154
|
+
if (o.before) args.push("-B", String(o.before));
|
|
155
|
+
if (o.after) args.push("-A", String(o.after));
|
|
156
|
+
}
|
|
157
|
+
if (input["-i"]) args.push("-i");
|
|
158
|
+
if (o.multiline) args.push("-U", "--multiline-dotall");
|
|
159
|
+
if (input.type) args.push("--type", input.type);
|
|
160
|
+
args.push("-e", input.pattern, "--");
|
|
161
|
+
|
|
162
|
+
/** @type {string[][]} */
|
|
163
|
+
let targetChunks = [[root]];
|
|
164
|
+
if (input.glob) {
|
|
165
|
+
// ripgrep searches explicitly named files regardless of --type/--glob,
|
|
166
|
+
// so the type filter goes on the listing, and the glob is ours.
|
|
167
|
+
let candidates = [root];
|
|
168
|
+
if (rootStat.isDirectory()) {
|
|
169
|
+
const listArgs = ["--files", "--no-config", "--no-messages", ...RG_EXCLUDES];
|
|
170
|
+
if (input.type) listArgs.push("--type", input.type);
|
|
171
|
+
const listed = await runRipgrep([...listArgs, "--", root], { cwd: ctx.cwd, signal: ctx.signal });
|
|
172
|
+
if (!listed.ok) return { ok: false, error: listed.error };
|
|
173
|
+
candidates = listed.stdout.split("\n").filter(Boolean);
|
|
174
|
+
}
|
|
175
|
+
const selected = candidates.filter((f) => matchGlob(globPath(f), input.glob));
|
|
176
|
+
targetChunks = [];
|
|
177
|
+
for (let i = 0; i < selected.length; i += RG_FILES_PER_CALL) targetChunks.push(selected.slice(i, i + RG_FILES_PER_CALL));
|
|
178
|
+
}
|
|
179
|
+
lines = [];
|
|
180
|
+
for (const chunk of targetChunks) {
|
|
181
|
+
if (ctx.signal?.aborted) break;
|
|
182
|
+
const r = await runRipgrep([...args, ...chunk], { cwd: ctx.cwd, signal: ctx.signal });
|
|
183
|
+
if (!r.ok) return { ok: false, error: r.error };
|
|
184
|
+
lines.push(...r.stdout.split("\n").filter((l) => l !== ""));
|
|
185
|
+
if (lines.length > headLimit * 4) break;
|
|
186
|
+
}
|
|
187
|
+
if (mode === "files_with_matches") lines = sortByMtime(lines);
|
|
188
|
+
} else {
|
|
189
|
+
let re;
|
|
190
|
+
try {
|
|
191
|
+
re = new RegExp(input.pattern, `g${input["-i"] ? "i" : ""}${o.multiline ? "s" : ""}`);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return { ok: false, error: `Invalid regular expression: ${e instanceof Error ? e.message : e}` };
|
|
194
|
+
}
|
|
195
|
+
const files = rootStat.isDirectory() ? walkFiles(root, { signal: ctx.signal }) : [root];
|
|
196
|
+
const exts = input.type ? TYPE_EXTENSIONS[input.type] : null;
|
|
197
|
+
const selected = files.filter((f) => {
|
|
198
|
+
if (exts && !exts.includes(path.extname(f))) return false;
|
|
199
|
+
if (input.glob && !matchGlob(globPath(f), input.glob)) return false;
|
|
200
|
+
return true;
|
|
201
|
+
});
|
|
202
|
+
lines = [];
|
|
203
|
+
const ordered = mode === "files_with_matches" ? sortByMtime(selected) : selected.sort();
|
|
204
|
+
for (const f of ordered) {
|
|
205
|
+
if (ctx.signal?.aborted) break;
|
|
206
|
+
let buf;
|
|
207
|
+
try {
|
|
208
|
+
if (statSync(f).size > MAX_SEARCH_BYTES) continue;
|
|
209
|
+
buf = readFileSync(f);
|
|
210
|
+
} catch {
|
|
211
|
+
continue; // unreadable or vanished file: skip, as ripgrep does with --no-messages
|
|
212
|
+
}
|
|
213
|
+
if (buf.subarray(0, 8192).includes(0)) continue; // binary
|
|
214
|
+
lines.push(...searchText(f, buf.toString("utf8"), re, o));
|
|
215
|
+
if (lines.length > headLimit * 4) break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (lines.length === 0) return { ok: true, content: "No matches found." };
|
|
220
|
+
const shown = lines.slice(0, headLimit);
|
|
221
|
+
const more = lines.length - shown.length;
|
|
222
|
+
return { ok: true, content: shown.join("\n") + (more > 0 ? `\n(output truncated at ${headLimit} lines)` : "") };
|
|
223
|
+
},
|
|
224
|
+
};
|