vellum 0.2.1 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/bun.lock +71 -100
- package/package.json +5 -3
- package/scripts/capture-x-graphql.ts +562 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
- package/scripts/test.sh +5 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/asset-materialize-tool.test.ts +16 -15
- package/src/__tests__/asset-search-tool.test.ts +23 -22
- package/src/__tests__/attachments-store.test.ts +56 -127
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
- package/src/__tests__/browser-skill-endstate.test.ts +4 -3
- package/src/__tests__/call-bridge.test.ts +385 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-orchestrator.test.ts +130 -4
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +459 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-store.test.ts +216 -1
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +305 -3
- package/src/__tests__/conflict-store.test.ts +2 -1
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/conversation-store.test.ts +30 -32
- package/src/__tests__/credential-security-invariants.test.ts +4 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/handlers-twilio-config.test.ts +221 -0
- package/src/__tests__/handlers-twitter-config.test.ts +718 -0
- package/src/__tests__/intent-routing.test.ts +64 -57
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +71 -28
- package/src/__tests__/llm-usage-store.test.ts +3 -8
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +100 -2
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/playbook-tools.test.ts +342 -0
- package/src/__tests__/profile-compiler.test.ts +2 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +303 -0
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
- package/src/__tests__/recurrence-engine.test.ts +69 -0
- package/src/__tests__/recurrence-types.test.ts +71 -0
- package/src/__tests__/registry.test.ts +5 -3
- package/src/__tests__/relay-server.test.ts +633 -0
- package/src/__tests__/reminder-store.test.ts +6 -3
- package/src/__tests__/reminder.test.ts +43 -77
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
- package/src/__tests__/run-orchestrator.test.ts +4 -4
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
- package/src/__tests__/runtime-runs-http.test.ts +4 -4
- package/src/__tests__/runtime-runs.test.ts +4 -4
- package/src/__tests__/schedule-store.test.ts +482 -0
- package/src/__tests__/schedule-tools.test.ts +700 -0
- package/src/__tests__/scheduler-recurrence.test.ts +329 -0
- package/src/__tests__/server-history-render.test.ts +14 -13
- package/src/__tests__/session-conflict-gate.test.ts +28 -25
- package/src/__tests__/session-error.test.ts +28 -0
- package/src/__tests__/session-init.benchmark.test.ts +462 -0
- package/src/__tests__/session-queue.test.ts +71 -48
- package/src/__tests__/session-runtime-assembly.test.ts +161 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/signup-e2e.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/subagent-manager-notify.test.ts +42 -10
- package/src/__tests__/subagent-tools.test.ts +141 -41
- package/src/__tests__/task-compiler.test.ts +2 -1
- package/src/__tests__/task-runner.test.ts +2 -1
- package/src/__tests__/task-scheduler.test.ts +2 -1
- package/src/__tests__/task-tools.test.ts +49 -56
- package/src/__tests__/tool-audit-listener.test.ts +1 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor.test.ts +13 -17
- package/src/__tests__/turn-commit.test.ts +218 -3
- package/src/__tests__/twilio-provider.test.ts +143 -0
- package/src/__tests__/twilio-routes.test.ts +789 -0
- package/src/__tests__/twitter-auth-handler.test.ts +581 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/workspace-git-service.test.ts +186 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +12 -8
- package/src/calls/__tests__/twilio-webhook-urls.test.ts +162 -0
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +43 -5
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +43 -17
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-store.ts +192 -5
- package/src/calls/relay-server.ts +41 -4
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-config.ts +8 -8
- package/src/calls/twilio-provider.ts +13 -9
- package/src/calls/twilio-routes.ts +90 -76
- package/src/calls/twilio-webhook-urls.ts +50 -0
- package/src/calls/types.ts +1 -1
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +251 -1
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/map.ts +270 -0
- package/src/cli/twitter.ts +575 -0
- package/src/cli.ts +7 -5
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
- package/src/config/bundled-skills/document/SKILL.md +18 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +82 -23
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/twitter/SKILL.md +134 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/defaults.ts +34 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +165 -1
- package/src/config/system-prompt.ts +61 -16
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +4 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -5
- package/src/contacts/contact-store.ts +4 -4
- package/src/daemon/assistant-attachments.ts +10 -0
- package/src/daemon/classifier.ts +3 -1
- package/src/daemon/computer-use-session.ts +3 -1
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +16 -1
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +7 -1
- package/src/daemon/handlers/config.ts +205 -5
- package/src/daemon/handlers/diagnostics.ts +5 -1
- package/src/daemon/handlers/documents.ts +18 -29
- package/src/daemon/handlers/home-base.ts +5 -1
- package/src/daemon/handlers/index.ts +40 -277
- package/src/daemon/handlers/misc.ts +9 -1
- package/src/daemon/handlers/publish.ts +6 -1
- package/src/daemon/handlers/sessions.ts +65 -12
- package/src/daemon/handlers/shared.ts +36 -1
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +20 -6
- package/src/daemon/handlers/subagents.ts +8 -3
- package/src/daemon/handlers/twitter-auth.ts +169 -0
- package/src/daemon/handlers/work-items.ts +384 -68
- package/src/daemon/ipc-contract-inventory.json +32 -4
- package/src/daemon/ipc-contract.ts +156 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +21 -0
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +75 -10
- package/src/daemon/server.ts +143 -26
- package/src/daemon/session-agent-loop.ts +922 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-conflict-gate.ts +18 -109
- package/src/daemon/session-error.ts +24 -3
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +164 -0
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-queue-manager.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +52 -0
- package/src/daemon/session-skill-tools.ts +124 -5
- package/src/daemon/session-slash.ts +3 -0
- package/src/daemon/session-surfaces.ts +77 -2
- package/src/daemon/session-tool-setup.ts +216 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1404
- package/src/daemon/video-thumbnail.ts +60 -0
- package/src/doordash/client.ts +121 -27
- package/src/doordash/queries.ts +1 -2
- package/src/export/formatter.ts +3 -1
- package/src/followups/followup-store.ts +4 -2
- package/src/followups/types.ts +6 -0
- package/src/hooks/templates.ts +1 -1
- package/src/index.ts +32 -1153
- package/src/memory/attachments-store.ts +28 -83
- package/src/memory/channel-delivery-store.ts +7 -21
- package/src/memory/clarification-resolver.ts +6 -5
- package/src/memory/conflict-intent.ts +114 -0
- package/src/memory/contradiction-checker.ts +3 -2
- package/src/memory/conversation-key-store.ts +10 -29
- package/src/memory/conversation-store.ts +2 -1
- package/src/memory/db.ts +96 -2
- package/src/memory/entity-extractor.ts +6 -3
- package/src/memory/items-extractor.ts +5 -4
- package/src/memory/job-handlers/conflict.ts +23 -1
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/llm-usage-store.ts +1 -2
- package/src/memory/runs-store.ts +1 -2
- package/src/memory/schema.ts +23 -2
- package/src/messaging/style-analyzer.ts +3 -2
- package/src/messaging/thread-summarizer.ts +8 -12
- package/src/messaging/triage-engine.ts +4 -2
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/registry.ts +8 -0
- package/src/runtime/gateway-client.ts +36 -0
- package/src/runtime/http-server.ts +166 -22
- package/src/runtime/routes/attachment-routes.ts +2 -3
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +125 -88
- package/src/runtime/routes/conversation-routes.ts +5 -5
- package/src/runtime/routes/run-routes.ts +2 -2
- package/src/runtime/run-orchestrator.ts +9 -3
- package/src/schedule/recurrence-engine.ts +138 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +102 -57
- package/src/schedule/scheduler.ts +9 -6
- package/src/security/oauth2.ts +29 -4
- package/src/security/secret-allowlist.ts +46 -0
- package/src/skills/clawhub.ts +1 -1
- package/src/subagent/manager.ts +40 -8
- package/src/swarm/backend-claude-code.ts +64 -9
- package/src/swarm/worker-prompts.ts +2 -1
- package/src/tasks/SPEC.md +34 -28
- package/src/tasks/ephemeral-permissions.ts +16 -7
- package/src/tasks/task-compiler.ts +5 -4
- package/src/tasks/task-runner.ts +10 -5
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/assets/search.ts +4 -4
- package/src/tools/browser/api-map.ts +293 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +2 -1
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/browser/network-recorder.ts +5 -4
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +17 -67
- package/src/tools/calls/call-start.ts +24 -85
- package/src/tools/calls/call-status.ts +35 -51
- package/src/tools/claude-code/claude-code.ts +207 -11
- package/src/tools/contacts/contact-merge.ts +46 -78
- package/src/tools/contacts/contact-search.ts +35 -79
- package/src/tools/contacts/contact-upsert.ts +35 -108
- package/src/tools/credentials/vault.ts +20 -4
- package/src/tools/document/document-tool.ts +71 -144
- package/src/tools/executor.ts +129 -10
- package/src/tools/followups/followup_create.ts +46 -88
- package/src/tools/followups/followup_list.ts +34 -74
- package/src/tools/followups/followup_resolve.ts +31 -66
- package/src/tools/host-terminal/cli-discover.ts +2 -1
- package/src/tools/host-terminal/host-shell.ts +10 -0
- package/src/tools/memory/handlers.ts +5 -4
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/web-fetch.ts +18 -6
- package/src/tools/playbooks/index.ts +4 -5
- package/src/tools/playbooks/playbook-create.ts +3 -47
- package/src/tools/playbooks/playbook-delete.ts +1 -25
- package/src/tools/playbooks/playbook-list.ts +1 -28
- package/src/tools/playbooks/playbook-update.ts +3 -51
- package/src/tools/reminder/reminder.ts +5 -78
- package/src/tools/schedule/create.ts +69 -74
- package/src/tools/schedule/delete.ts +21 -47
- package/src/tools/schedule/list.ts +55 -74
- package/src/tools/schedule/update.ts +77 -84
- package/src/tools/subagent/abort.ts +29 -58
- package/src/tools/subagent/message.ts +30 -63
- package/src/tools/subagent/read.ts +53 -84
- package/src/tools/subagent/spawn.ts +43 -82
- package/src/tools/subagent/status.ts +42 -71
- package/src/tools/swarm/delegate.ts +2 -1
- package/src/tools/tasks/index.ts +8 -8
- package/src/tools/tasks/task-delete.ts +60 -88
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +72 -108
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +183 -215
- package/src/tools/tasks/work-item-list.ts +33 -63
- package/src/tools/tasks/work-item-remove.ts +45 -97
- package/src/tools/tasks/work-item-update.ts +91 -163
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +0 -62
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/tools/watch/screen-watch.ts +3 -1
- package/src/tools/watcher/create.ts +52 -98
- package/src/tools/watcher/delete.ts +20 -46
- package/src/tools/watcher/digest.ts +36 -70
- package/src/tools/watcher/list.ts +49 -79
- package/src/tools/watcher/update.ts +45 -91
- package/src/twitter/client.ts +690 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/types.ts +0 -1
- package/src/util/truncate.ts +6 -0
- package/src/watcher/providers/slack.ts +2 -1
- package/src/watcher/watcher-store.ts +3 -2
- package/src/work-items/work-item-store.ts +27 -2
- package/src/workspace/commit-message-enrichment-service.ts +31 -7
- package/src/workspace/git-service.ts +87 -22
- package/src/workspace/provider-commit-message-generator.ts +269 -0
- package/src/workspace/turn-commit.ts +62 -3
- package/src/tools/contacts/index.ts +0 -4
- package/src/tools/document/index.ts +0 -5
- package/src/tools/followups/index.ts +0 -3
- package/src/tools/subagent/index.ts +0 -5
- /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import {
|
|
6
|
+
discoverCCCommands,
|
|
7
|
+
getCCCommand,
|
|
8
|
+
loadCCCommandTemplate,
|
|
9
|
+
invalidateCCCommandCache,
|
|
10
|
+
} from '../cc-command-registry.js';
|
|
11
|
+
|
|
12
|
+
let tmpDir: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'cc-cmd-test-'));
|
|
16
|
+
invalidateCCCommandCache();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
+
invalidateCCCommandCache();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/** Helper to create a .claude/commands/ directory with markdown files. */
|
|
25
|
+
function createCommandsDir(base: string, files: Record<string, string>): void {
|
|
26
|
+
const commandsDir = join(base, '.claude', 'commands');
|
|
27
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
28
|
+
for (const [name, content] of Object.entries(files)) {
|
|
29
|
+
writeFileSync(join(commandsDir, name), content, 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('discoverCCCommands', () => {
|
|
34
|
+
test('discovers commands in .claude/commands/', () => {
|
|
35
|
+
createCommandsDir(tmpDir, {
|
|
36
|
+
'hello.md': '# Hello World\nThis is the hello command.',
|
|
37
|
+
'deploy.md': 'Deploy the application to production.',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const registry = discoverCCCommands(tmpDir);
|
|
41
|
+
expect(registry.entries.size).toBe(2);
|
|
42
|
+
|
|
43
|
+
const hello = registry.entries.get('hello');
|
|
44
|
+
expect(hello).toBeDefined();
|
|
45
|
+
expect(hello!.name).toBe('hello');
|
|
46
|
+
expect(hello!.summary).toBe('Hello World');
|
|
47
|
+
expect(hello!.source).toBe(tmpDir);
|
|
48
|
+
|
|
49
|
+
const deploy = registry.entries.get('deploy');
|
|
50
|
+
expect(deploy).toBeDefined();
|
|
51
|
+
expect(deploy!.name).toBe('deploy');
|
|
52
|
+
expect(deploy!.summary).toBe('Deploy the application to production.');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('child directory commands override parent on name collisions', () => {
|
|
56
|
+
// Create parent commands
|
|
57
|
+
createCommandsDir(tmpDir, {
|
|
58
|
+
'shared.md': 'Parent version of shared command.',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Create child directory with overriding command
|
|
62
|
+
const childDir = join(tmpDir, 'project');
|
|
63
|
+
mkdirSync(childDir, { recursive: true });
|
|
64
|
+
createCommandsDir(childDir, {
|
|
65
|
+
'shared.md': 'Child version of shared command.',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const registry = discoverCCCommands(childDir);
|
|
69
|
+
const shared = registry.entries.get('shared');
|
|
70
|
+
expect(shared).toBeDefined();
|
|
71
|
+
expect(shared!.summary).toBe('Child version of shared command.');
|
|
72
|
+
expect(shared!.source).toBe(childDir);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('invalid filenames are skipped', () => {
|
|
76
|
+
createCommandsDir(tmpDir, {
|
|
77
|
+
'valid-name.md': 'A valid command.',
|
|
78
|
+
'.hidden.md': 'Hidden file should be skipped.',
|
|
79
|
+
'-starts-with-dash.md': 'Invalid start character.',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const registry = discoverCCCommands(tmpDir);
|
|
83
|
+
expect(registry.entries.size).toBe(1);
|
|
84
|
+
expect(registry.entries.has('valid-name')).toBe(true);
|
|
85
|
+
expect(registry.entries.has('.hidden')).toBe(false);
|
|
86
|
+
expect(registry.entries.has('-starts-with-dash')).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('non-.md files are ignored', () => {
|
|
90
|
+
createCommandsDir(tmpDir, {
|
|
91
|
+
'readme.txt': 'Not a markdown file.',
|
|
92
|
+
'command.md': 'A real command.',
|
|
93
|
+
'notes.json': '{}',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const registry = discoverCCCommands(tmpDir);
|
|
97
|
+
expect(registry.entries.size).toBe(1);
|
|
98
|
+
expect(registry.entries.has('command')).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('empty directory returns empty registry', () => {
|
|
102
|
+
const commandsDir = join(tmpDir, '.claude', 'commands');
|
|
103
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
104
|
+
|
|
105
|
+
const registry = discoverCCCommands(tmpDir);
|
|
106
|
+
expect(registry.entries.size).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('no .claude/commands/ directory returns empty registry', () => {
|
|
110
|
+
const registry = discoverCCCommands(tmpDir);
|
|
111
|
+
expect(registry.entries.size).toBe(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('commands from multiple ancestor levels are merged', () => {
|
|
115
|
+
// Parent has a unique command
|
|
116
|
+
createCommandsDir(tmpDir, {
|
|
117
|
+
'parent-only.md': 'Only in parent.',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Child has a different command
|
|
121
|
+
const childDir = join(tmpDir, 'child');
|
|
122
|
+
mkdirSync(childDir, { recursive: true });
|
|
123
|
+
createCommandsDir(childDir, {
|
|
124
|
+
'child-only.md': 'Only in child.',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const registry = discoverCCCommands(childDir);
|
|
128
|
+
expect(registry.entries.size).toBe(2);
|
|
129
|
+
expect(registry.entries.has('parent-only')).toBe(true);
|
|
130
|
+
expect(registry.entries.has('child-only')).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('caching', () => {
|
|
135
|
+
test('cache returns same instance within TTL', () => {
|
|
136
|
+
createCommandsDir(tmpDir, {
|
|
137
|
+
'test.md': 'Test command.',
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const first = discoverCCCommands(tmpDir);
|
|
141
|
+
const second = discoverCCCommands(tmpDir);
|
|
142
|
+
expect(first).toBe(second); // same object reference
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('invalidateCCCommandCache forces re-discovery', () => {
|
|
146
|
+
createCommandsDir(tmpDir, {
|
|
147
|
+
'test.md': 'Test command.',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const first = discoverCCCommands(tmpDir);
|
|
151
|
+
|
|
152
|
+
invalidateCCCommandCache();
|
|
153
|
+
|
|
154
|
+
const second = discoverCCCommands(tmpDir);
|
|
155
|
+
expect(first).not.toBe(second); // different object reference
|
|
156
|
+
expect(second.entries.size).toBe(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('expired TTL forces re-discovery', () => {
|
|
160
|
+
createCommandsDir(tmpDir, {
|
|
161
|
+
'test.md': 'Test command.',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Use a very short TTL
|
|
165
|
+
const first = discoverCCCommands(tmpDir, 0);
|
|
166
|
+
const second = discoverCCCommands(tmpDir, 0);
|
|
167
|
+
expect(first).not.toBe(second); // different object reference due to expired TTL
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('getCCCommand', () => {
|
|
172
|
+
test('looks up command by name (case-insensitive)', () => {
|
|
173
|
+
createCommandsDir(tmpDir, {
|
|
174
|
+
'MyCommand.md': 'My command description.',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const entry = getCCCommand(tmpDir, 'mycommand');
|
|
178
|
+
expect(entry).toBeDefined();
|
|
179
|
+
expect(entry!.name).toBe('MyCommand');
|
|
180
|
+
|
|
181
|
+
const entryUpper = getCCCommand(tmpDir, 'MYCOMMAND');
|
|
182
|
+
expect(entryUpper).toBeDefined();
|
|
183
|
+
expect(entryUpper!.name).toBe('MyCommand');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('returns undefined for non-existent command', () => {
|
|
187
|
+
createCommandsDir(tmpDir, {
|
|
188
|
+
'exists.md': 'I exist.',
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const entry = getCCCommand(tmpDir, 'nonexistent');
|
|
192
|
+
expect(entry).toBeUndefined();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('loadCCCommandTemplate', () => {
|
|
197
|
+
test('reads full file content at execution time', () => {
|
|
198
|
+
const fullContent = '---\ntitle: Test\n---\n\n# Test Command\n\nThis is the full template body.\n\n## Arguments\n- arg1: required\n- arg2: optional\n';
|
|
199
|
+
createCommandsDir(tmpDir, {
|
|
200
|
+
'test.md': fullContent,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const registry = discoverCCCommands(tmpDir);
|
|
204
|
+
const entry = registry.entries.get('test')!;
|
|
205
|
+
expect(entry).toBeDefined();
|
|
206
|
+
|
|
207
|
+
const template = loadCCCommandTemplate(entry);
|
|
208
|
+
expect(template).toBe(fullContent);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('summary extraction', () => {
|
|
213
|
+
test('skips YAML frontmatter', () => {
|
|
214
|
+
createCommandsDir(tmpDir, {
|
|
215
|
+
'with-frontmatter.md': '---\ntitle: My Command\nauthor: test\n---\n\nActual summary line.',
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const registry = discoverCCCommands(tmpDir);
|
|
219
|
+
const entry = registry.entries.get('with-frontmatter');
|
|
220
|
+
expect(entry).toBeDefined();
|
|
221
|
+
expect(entry!.summary).toBe('Actual summary line.');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('strips heading markers', () => {
|
|
225
|
+
createCommandsDir(tmpDir, {
|
|
226
|
+
'heading.md': '## This is a heading',
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const registry = discoverCCCommands(tmpDir);
|
|
230
|
+
const entry = registry.entries.get('heading');
|
|
231
|
+
expect(entry).toBeDefined();
|
|
232
|
+
expect(entry!.summary).toBe('This is a heading');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('strips multiple heading levels', () => {
|
|
236
|
+
createCommandsDir(tmpDir, {
|
|
237
|
+
'h1.md': '# H1 Heading',
|
|
238
|
+
'h3.md': '### H3 Heading',
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const registry = discoverCCCommands(tmpDir);
|
|
242
|
+
expect(registry.entries.get('h1')!.summary).toBe('H1 Heading');
|
|
243
|
+
expect(registry.entries.get('h3')!.summary).toBe('H3 Heading');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('skips empty lines before summary', () => {
|
|
247
|
+
createCommandsDir(tmpDir, {
|
|
248
|
+
'empty-lines.md': '\n\n\nFirst real line.',
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const registry = discoverCCCommands(tmpDir);
|
|
252
|
+
expect(registry.entries.get('empty-lines')!.summary).toBe('First real line.');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('truncates summary to 100 characters', () => {
|
|
256
|
+
const longLine = 'A'.repeat(150);
|
|
257
|
+
createCommandsDir(tmpDir, {
|
|
258
|
+
'long.md': longLine,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const registry = discoverCCCommands(tmpDir);
|
|
262
|
+
const entry = registry.entries.get('long');
|
|
263
|
+
expect(entry).toBeDefined();
|
|
264
|
+
expect(entry!.summary.length).toBe(100);
|
|
265
|
+
expect(entry!.summary).toBe('A'.repeat(100));
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('handles file with only frontmatter and no content', () => {
|
|
269
|
+
createCommandsDir(tmpDir, {
|
|
270
|
+
'empty-body.md': '---\ntitle: Empty\n---\n',
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const registry = discoverCCCommands(tmpDir);
|
|
274
|
+
const entry = registry.entries.get('empty-body');
|
|
275
|
+
expect(entry).toBeDefined();
|
|
276
|
+
expect(entry!.summary).toBe('');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('handles frontmatter with Windows-style line endings', () => {
|
|
280
|
+
createCommandsDir(tmpDir, {
|
|
281
|
+
'crlf.md': '---\r\ntitle: Test\r\n---\r\n\r\nSummary with CRLF.',
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const registry = discoverCCCommands(tmpDir);
|
|
285
|
+
const entry = registry.entries.get('crlf');
|
|
286
|
+
expect(entry).toBeDefined();
|
|
287
|
+
expect(entry!.summary).toBe('Summary with CRLF.');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('command name validation', () => {
|
|
292
|
+
test('accepts valid names with dots, dashes, underscores', () => {
|
|
293
|
+
createCommandsDir(tmpDir, {
|
|
294
|
+
'my-command.md': 'Dashed name.',
|
|
295
|
+
'my_command.md': 'Underscored name.',
|
|
296
|
+
'my.command.md': 'Dotted name.',
|
|
297
|
+
'Command123.md': 'Alphanumeric.',
|
|
298
|
+
'a.md': 'Single char.',
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const registry = discoverCCCommands(tmpDir);
|
|
302
|
+
expect(registry.entries.has('my-command')).toBe(true);
|
|
303
|
+
expect(registry.entries.has('my_command')).toBe(true);
|
|
304
|
+
expect(registry.entries.has('my.command')).toBe(true);
|
|
305
|
+
expect(registry.entries.has('command123')).toBe(true);
|
|
306
|
+
expect(registry.entries.has('a')).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('rejects names starting with special characters', () => {
|
|
310
|
+
createCommandsDir(tmpDir, {
|
|
311
|
+
'_start.md': 'Starts with underscore.',
|
|
312
|
+
'.start.md': 'Starts with dot.',
|
|
313
|
+
'-start.md': 'Starts with dash.',
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const registry = discoverCCCommands(tmpDir);
|
|
317
|
+
expect(registry.entries.size).toBe(0);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { getLogger } from '../util/logger.js';
|
|
4
|
+
|
|
5
|
+
const log = getLogger('cc-commands');
|
|
6
|
+
|
|
7
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export interface CCCommandEntry {
|
|
10
|
+
/** Command name: basename without .md extension. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** First non-empty line after frontmatter, stripped of heading markers. */
|
|
13
|
+
summary: string;
|
|
14
|
+
/** Absolute path to the .md file. */
|
|
15
|
+
filePath: string;
|
|
16
|
+
/** Directory containing the `.claude/commands/` folder. */
|
|
17
|
+
source: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CCCommandRegistry {
|
|
21
|
+
/** Commands keyed by lowercase name. */
|
|
22
|
+
entries: Map<string, CCCommandEntry>;
|
|
23
|
+
/** Timestamp (ms) when discovery was performed. */
|
|
24
|
+
discoveredAt: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const COMMAND_NAME_REGEX = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
30
|
+
const FRONTMATTER_REGEX = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/;
|
|
31
|
+
const DEFAULT_CACHE_TTL_MS = 30_000;
|
|
32
|
+
const MAX_SUMMARY_LENGTH = 100;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Maximum bytes to read from each command file during discovery.
|
|
36
|
+
* 1 KiB is enough for frontmatter (typically < 200 B) plus several content
|
|
37
|
+
* lines, which is all we need to extract a one-line summary.
|
|
38
|
+
*/
|
|
39
|
+
const SUMMARY_READ_BYTES = 1024;
|
|
40
|
+
|
|
41
|
+
// ─── Cache ───────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const cache = new Map<string, CCCommandRegistry>();
|
|
44
|
+
|
|
45
|
+
/** Clear all cached registries. */
|
|
46
|
+
export function invalidateCCCommandCache(): void {
|
|
47
|
+
cache.clear();
|
|
48
|
+
log.debug('CC command cache invalidated');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Partial I/O ─────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read at most `maxBytes` from the beginning of a file.
|
|
55
|
+
* Uses low-level `openSync`/`readSync` so we never pull the entire file into
|
|
56
|
+
* memory — important when command templates are large but we only need the
|
|
57
|
+
* first few lines for summary extraction.
|
|
58
|
+
*/
|
|
59
|
+
function readFileHead(filePath: string, maxBytes: number): string {
|
|
60
|
+
const fd = openSync(filePath, 'r');
|
|
61
|
+
try {
|
|
62
|
+
const buf = Buffer.alloc(maxBytes);
|
|
63
|
+
const bytesRead = readSync(fd, buf, 0, maxBytes, 0);
|
|
64
|
+
return buf.toString('utf-8', 0, bytesRead);
|
|
65
|
+
} finally {
|
|
66
|
+
closeSync(fd);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Summary extraction ──────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extract a one-line summary from the beginning of a markdown file.
|
|
74
|
+
* Skips YAML frontmatter if present, then returns the first non-empty line
|
|
75
|
+
* with leading `#` heading markers stripped. Truncates to 100 chars.
|
|
76
|
+
*/
|
|
77
|
+
function extractSummary(content: string): string {
|
|
78
|
+
// Strip frontmatter if present
|
|
79
|
+
let body = content;
|
|
80
|
+
const fmMatch = content.match(FRONTMATTER_REGEX);
|
|
81
|
+
if (fmMatch) {
|
|
82
|
+
body = content.slice(fmMatch[0].length);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Find first non-empty line
|
|
86
|
+
const lines = body.split(/\r?\n/);
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
if (!trimmed) continue;
|
|
90
|
+
|
|
91
|
+
// Strip leading # heading markers
|
|
92
|
+
const stripped = trimmed.replace(/^#+\s*/, '');
|
|
93
|
+
if (!stripped) continue;
|
|
94
|
+
|
|
95
|
+
// Truncate if needed
|
|
96
|
+
if (stripped.length > MAX_SUMMARY_LENGTH) {
|
|
97
|
+
return stripped.slice(0, MAX_SUMMARY_LENGTH);
|
|
98
|
+
}
|
|
99
|
+
return stripped;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── Discovery ───────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Discover `.claude/commands/*.md` files by walking up from `cwd`.
|
|
109
|
+
* Nearest directory wins on name collisions (child overrides parent).
|
|
110
|
+
* Results are cached per cwd with a 30-second TTL.
|
|
111
|
+
*/
|
|
112
|
+
export function discoverCCCommands(cwd: string, ttlMs: number = DEFAULT_CACHE_TTL_MS): CCCommandRegistry {
|
|
113
|
+
const resolvedCwd = resolve(cwd);
|
|
114
|
+
|
|
115
|
+
// Check cache
|
|
116
|
+
const cached = cache.get(resolvedCwd);
|
|
117
|
+
if (cached && (Date.now() - cached.discoveredAt) < ttlMs) {
|
|
118
|
+
log.debug({ cwd: resolvedCwd }, 'CC command cache hit');
|
|
119
|
+
return cached;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
log.debug({ cwd: resolvedCwd }, 'CC command cache miss, discovering commands');
|
|
123
|
+
|
|
124
|
+
const entries = new Map<string, CCCommandEntry>();
|
|
125
|
+
let current = resolvedCwd;
|
|
126
|
+
|
|
127
|
+
// Walk up the directory tree; collect commands from each level.
|
|
128
|
+
// Since child directories should win on name collisions, we only add entries
|
|
129
|
+
// that haven't been seen yet (first occurrence = nearest ancestor).
|
|
130
|
+
while (true) {
|
|
131
|
+
const commandsDir = join(current, '.claude', 'commands');
|
|
132
|
+
|
|
133
|
+
if (existsSync(commandsDir)) {
|
|
134
|
+
try {
|
|
135
|
+
const files = readdirSync(commandsDir, { withFileTypes: true });
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
if (!file.isFile()) continue;
|
|
138
|
+
if (!file.name.endsWith('.md')) continue;
|
|
139
|
+
|
|
140
|
+
const nameWithoutExt = basename(file.name, '.md');
|
|
141
|
+
|
|
142
|
+
// Validate command name
|
|
143
|
+
if (!COMMAND_NAME_REGEX.test(nameWithoutExt)) {
|
|
144
|
+
log.warn({ fileName: file.name, dir: commandsDir }, 'Skipping invalid CC command filename');
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const key = nameWithoutExt.toLowerCase();
|
|
149
|
+
|
|
150
|
+
// Child directories win — skip if already discovered from a closer ancestor
|
|
151
|
+
if (entries.has(key)) continue;
|
|
152
|
+
|
|
153
|
+
const filePath = join(commandsDir, file.name);
|
|
154
|
+
|
|
155
|
+
let summary = '';
|
|
156
|
+
try {
|
|
157
|
+
const head = readFileHead(filePath, SUMMARY_READ_BYTES);
|
|
158
|
+
summary = extractSummary(head);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
log.warn({ err, filePath }, 'Failed to read CC command file for summary extraction');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
entries.set(key, {
|
|
164
|
+
name: nameWithoutExt,
|
|
165
|
+
summary,
|
|
166
|
+
filePath,
|
|
167
|
+
source: current,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
log.warn({ err, commandsDir }, 'Failed to read CC commands directory');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const parent = dirname(current);
|
|
176
|
+
if (parent === current) break; // reached filesystem root
|
|
177
|
+
current = parent;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
log.debug({ cwd: resolvedCwd, count: entries.size }, 'CC command discovery complete');
|
|
181
|
+
|
|
182
|
+
const registry: CCCommandRegistry = {
|
|
183
|
+
entries,
|
|
184
|
+
discoveredAt: Date.now(),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
cache.set(resolvedCwd, registry);
|
|
188
|
+
return registry;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Lookup ──────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Look up a single CC command by name (case-insensitive).
|
|
195
|
+
*/
|
|
196
|
+
export function getCCCommand(cwd: string, name: string): CCCommandEntry | undefined {
|
|
197
|
+
const registry = discoverCCCommands(cwd);
|
|
198
|
+
return registry.entries.get(name.toLowerCase());
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── Template loading ────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Load the full markdown content of a CC command file.
|
|
205
|
+
* This is deferred to execution time to avoid reading full files during discovery.
|
|
206
|
+
*/
|
|
207
|
+
export function loadCCCommandTemplate(entry: CCCommandEntry): string {
|
|
208
|
+
return readFileSync(entry.filePath, 'utf-8');
|
|
209
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: "Contacts"
|
|
3
|
+
description: "Contact and relationship graph with multi-channel tracking and importance scoring"
|
|
4
|
+
metadata: {"vellum": {"emoji": "\ud83d\udcca"}}
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Manage the user's contact and relationship graph. Each contact can have multiple communication channels, an importance score, and interaction tracking.
|
|
8
|
+
|
|
9
|
+
## Contact Fields
|
|
10
|
+
|
|
11
|
+
- **display_name** -- the contact's name (required)
|
|
12
|
+
- **relationship** -- e.g. colleague, friend, manager, client, family
|
|
13
|
+
- **importance** -- score from 0 to 1 (default 0.5), higher means more important
|
|
14
|
+
- **response_expectation** -- expected response speed: immediate, within_hours, within_day, casual
|
|
15
|
+
- **preferred_tone** -- communication tone: formal, casual, friendly, professional
|
|
16
|
+
- **channels** -- list of communication channels (email, slack, whatsapp, phone, telegram, discord, other)
|
|
17
|
+
|
|
18
|
+
## Channel Types
|
|
19
|
+
|
|
20
|
+
Supported channel types: `email`, `slack`, `whatsapp`, `phone`, `telegram`, `discord`, `other`
|
|
21
|
+
|
|
22
|
+
Each channel has:
|
|
23
|
+
- **type** -- one of the supported channel types
|
|
24
|
+
- **address** -- the channel-specific identifier (email address, phone number, handle, etc.)
|
|
25
|
+
- **is_primary** -- whether this is the primary channel for its type
|
|
26
|
+
|
|
27
|
+
## Merging Contacts
|
|
28
|
+
|
|
29
|
+
When you discover two contacts are the same person (e.g. same person on email and Slack), use `contact_merge` to consolidate them. Merging:
|
|
30
|
+
- Combines all channels from both contacts
|
|
31
|
+
- Keeps the higher importance score
|
|
32
|
+
- Sums interaction counts
|
|
33
|
+
- Deletes the donor contact
|
|
34
|
+
|
|
35
|
+
## Tips
|
|
36
|
+
|
|
37
|
+
- Use `contact_search` with `channel_address` to find contacts by their email, phone, or handle.
|
|
38
|
+
- When creating follow-ups, provide a `contact_id` to link the follow-up to a specific contact for grace period calculations.
|
|
39
|
+
- Contacts with higher importance scores get shorter default response deadlines.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"tools": [
|
|
4
|
+
{
|
|
5
|
+
"name": "contact_upsert",
|
|
6
|
+
"description": "Create or update a contact in the relationship graph. Use this to track people the user interacts with across channels (email, Slack, etc.).",
|
|
7
|
+
"category": "contacts",
|
|
8
|
+
"risk": "low",
|
|
9
|
+
"input_schema": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"id": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Contact ID to update. Omit to create a new contact (or auto-match by channel address)."
|
|
15
|
+
},
|
|
16
|
+
"display_name": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Display name for the contact"
|
|
19
|
+
},
|
|
20
|
+
"relationship": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Relationship type (e.g. colleague, friend, manager, client, family)"
|
|
23
|
+
},
|
|
24
|
+
"importance": {
|
|
25
|
+
"type": "number",
|
|
26
|
+
"description": "Importance score 0-1 (default 0.5). Higher = more important."
|
|
27
|
+
},
|
|
28
|
+
"response_expectation": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Expected response speed (e.g. immediate, within_hours, within_day, casual)"
|
|
31
|
+
},
|
|
32
|
+
"preferred_tone": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "Preferred communication tone (e.g. formal, casual, friendly, professional)"
|
|
35
|
+
},
|
|
36
|
+
"channels": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"description": "Communication channels for this contact",
|
|
39
|
+
"items": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"type": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"enum": ["email", "slack", "whatsapp", "phone", "telegram", "discord", "other"],
|
|
45
|
+
"description": "Channel type"
|
|
46
|
+
},
|
|
47
|
+
"address": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "Channel address (email address, Slack handle, phone number, etc.)"
|
|
50
|
+
},
|
|
51
|
+
"is_primary": {
|
|
52
|
+
"type": "boolean",
|
|
53
|
+
"description": "Whether this is the primary channel for this type"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"required": ["type", "address"]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"required": ["display_name"]
|
|
61
|
+
},
|
|
62
|
+
"executor": "tools/contact-upsert.ts",
|
|
63
|
+
"execution_target": "host"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"name": "contact_search",
|
|
67
|
+
"description": "Search for contacts by name, channel address, relationship type, or other criteria. Returns matching contacts with their channel information.",
|
|
68
|
+
"category": "contacts",
|
|
69
|
+
"risk": "low",
|
|
70
|
+
"input_schema": {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {
|
|
73
|
+
"query": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"description": "Search by display name (partial match)"
|
|
76
|
+
},
|
|
77
|
+
"channel_address": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"description": "Search by channel address (email, phone, handle — partial match)"
|
|
80
|
+
},
|
|
81
|
+
"channel_type": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "Filter by channel type when searching by address (email, slack, whatsapp, phone, etc.)"
|
|
84
|
+
},
|
|
85
|
+
"relationship": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"description": "Filter by relationship type (exact match)"
|
|
88
|
+
},
|
|
89
|
+
"limit": {
|
|
90
|
+
"type": "number",
|
|
91
|
+
"description": "Maximum results to return (default 20, max 100)"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"required": []
|
|
95
|
+
},
|
|
96
|
+
"executor": "tools/contact-search.ts",
|
|
97
|
+
"execution_target": "host"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"name": "contact_merge",
|
|
101
|
+
"description": "Merge two contacts when you discover they are the same person (e.g. same person on email and Slack). Combines channels, keeps the higher importance, and deletes the donor contact.",
|
|
102
|
+
"category": "contacts",
|
|
103
|
+
"risk": "medium",
|
|
104
|
+
"input_schema": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"keep_id": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "ID of the contact to keep (the surviving contact)"
|
|
110
|
+
},
|
|
111
|
+
"merge_id": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"description": "ID of the contact to merge into the kept contact (will be deleted)"
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"required": ["keep_id", "merge_id"]
|
|
117
|
+
},
|
|
118
|
+
"executor": "tools/contact-merge.ts",
|
|
119
|
+
"execution_target": "host"
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|