@adecore/agents 0.0.1 → 0.17.0-beta.2
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/LICENSE +110 -0
- package/README.md +48 -0
- package/dist/async.d.ts +2 -0
- package/dist/async.js +31 -0
- package/dist/chat/attachment-store.d.ts +10 -0
- package/dist/chat/attachment-store.js +93 -0
- package/dist/chat/backend.d.ts +214 -0
- package/dist/chat/backend.js +1 -0
- package/dist/chat/background-work.d.ts +8 -0
- package/dist/chat/background-work.js +33 -0
- package/dist/chat/bookmark-store.d.ts +25 -0
- package/dist/chat/bookmark-store.js +141 -0
- package/dist/chat/chat-core.d.ts +311 -0
- package/dist/chat/chat-core.js +1025 -0
- package/dist/chat/chat-log.d.ts +33 -0
- package/dist/chat/chat-log.js +163 -0
- package/dist/chat/chat-process.d.ts +46 -0
- package/dist/chat/chat-process.js +190 -0
- package/dist/chat/chat-session.d.ts +209 -0
- package/dist/chat/chat-session.js +1511 -0
- package/dist/chat/chat-store.d.ts +37 -0
- package/dist/chat/chat-store.js +202 -0
- package/dist/chat/chat-title.d.ts +10 -0
- package/dist/chat/chat-title.js +74 -0
- package/dist/chat/claude-backend.d.ts +35 -0
- package/dist/chat/claude-backend.js +214 -0
- package/dist/chat/claude-protocol.d.ts +41 -0
- package/dist/chat/claude-protocol.js +642 -0
- package/dist/chat/claude-title.d.ts +11 -0
- package/dist/chat/claude-title.js +97 -0
- package/dist/chat/claude-transcript.d.ts +28 -0
- package/dist/chat/claude-transcript.js +211 -0
- package/dist/chat/codex-backend.d.ts +51 -0
- package/dist/chat/codex-backend.js +364 -0
- package/dist/chat/codex-protocol.d.ts +56 -0
- package/dist/chat/codex-protocol.js +767 -0
- package/dist/chat/codex-thread.d.ts +32 -0
- package/dist/chat/codex-thread.js +131 -0
- package/dist/chat/codex-transport.d.ts +42 -0
- package/dist/chat/codex-transport.js +188 -0
- package/dist/chat/composer-preferences.d.ts +15 -0
- package/dist/chat/composer-preferences.js +46 -0
- package/dist/chat/context-breakdown.d.ts +2 -0
- package/dist/chat/context-breakdown.js +74 -0
- package/dist/chat/delta-coalescer.d.ts +12 -0
- package/dist/chat/delta-coalescer.js +55 -0
- package/dist/chat/errors.d.ts +4 -0
- package/dist/chat/errors.js +4 -0
- package/dist/chat/fake-claude.d.ts +3 -0
- package/dist/chat/fake-claude.js +982 -0
- package/dist/chat/fake-cli.d.ts +25 -0
- package/dist/chat/fake-cli.js +157 -0
- package/dist/chat/fake-codex.d.ts +10 -0
- package/dist/chat/fake-codex.js +502 -0
- package/dist/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
- package/dist/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
- package/dist/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
- package/dist/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
- package/dist/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
- package/dist/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
- package/dist/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
- package/dist/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
- package/dist/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
- package/dist/chat/input.d.ts +19 -0
- package/dist/chat/input.js +71 -0
- package/dist/chat/limit-resume.d.ts +11 -0
- package/dist/chat/limit-resume.js +68 -0
- package/dist/chat/projector.d.ts +60 -0
- package/dist/chat/projector.js +860 -0
- package/dist/chat/request-summary.d.ts +3 -0
- package/dist/chat/request-summary.js +143 -0
- package/dist/chat/subagent-projection.d.ts +8 -0
- package/dist/chat/subagent-projection.js +40 -0
- package/dist/chat/subagent-reader.d.ts +66 -0
- package/dist/chat/subagent-reader.js +396 -0
- package/dist/chat/subagent-settlement.d.ts +6 -0
- package/dist/chat/subagent-settlement.js +103 -0
- package/dist/chat/thread.d.ts +30 -0
- package/dist/chat/thread.js +147 -0
- package/dist/chat/wake-chat.d.ts +20 -0
- package/dist/chat/wake-chat.js +27 -0
- package/dist/client-sinks.d.ts +9 -0
- package/dist/client-sinks.js +34 -0
- package/dist/coded-error.d.ts +5 -0
- package/dist/coded-error.js +17 -0
- package/dist/context/argv.d.ts +11 -0
- package/dist/context/argv.js +58 -0
- package/dist/context/refusal.d.ts +9 -0
- package/dist/context/refusal.js +38 -0
- package/dist/context/verb.d.ts +85 -0
- package/dist/context/verb.js +233 -0
- package/dist/error-text.d.ts +5 -0
- package/dist/error-text.js +24 -0
- package/dist/events.d.ts +12 -0
- package/dist/events.js +1 -0
- package/dist/fs.d.ts +9 -0
- package/dist/fs.js +107 -0
- package/dist/host/agent-host.d.ts +28 -0
- package/dist/host/agent-host.js +105 -0
- package/dist/host/environment.d.ts +7 -0
- package/dist/host/environment.js +22 -0
- package/dist/host/handlers.d.ts +30 -0
- package/dist/host/handlers.js +111 -0
- package/dist/host/memory-port.d.ts +2 -0
- package/dist/host/memory-port.js +25 -0
- package/dist/host/wiring.d.ts +40 -0
- package/dist/host/wiring.js +120 -0
- package/dist/lineage.d.ts +44 -0
- package/dist/lineage.js +134 -0
- package/dist/messages/deliver-message.d.ts +21 -0
- package/dist/messages/deliver-message.js +42 -0
- package/dist/messages/deliver-notice.d.ts +25 -0
- package/dist/messages/deliver-notice.js +75 -0
- package/dist/messages/notice-notes.d.ts +15 -0
- package/dist/messages/notice-notes.js +26 -0
- package/dist/messages/notice-store.d.ts +30 -0
- package/dist/messages/notice-store.js +120 -0
- package/dist/modes.d.ts +11 -0
- package/dist/modes.js +28 -0
- package/dist/outbox/manual-clock.d.ts +10 -0
- package/dist/outbox/manual-clock.js +25 -0
- package/dist/outbox/outbox-worker.d.ts +44 -0
- package/dist/outbox/outbox-worker.js +173 -0
- package/dist/outbox/outbox.d.ts +37 -0
- package/dist/outbox/outbox.js +76 -0
- package/dist/providers/accounts/accounts.d.ts +20 -0
- package/dist/providers/accounts/accounts.js +158 -0
- package/dist/providers/accounts/launch.d.ts +17 -0
- package/dist/providers/accounts/launch.js +30 -0
- package/dist/providers/accounts/service.d.ts +87 -0
- package/dist/providers/accounts/service.js +538 -0
- package/dist/providers/accounts/shadow-home.d.ts +8 -0
- package/dist/providers/accounts/shadow-home.js +70 -0
- package/dist/providers/accounts/status.d.ts +12 -0
- package/dist/providers/accounts/status.js +85 -0
- package/dist/providers/accounts/store.d.ts +14 -0
- package/dist/providers/accounts/store.js +112 -0
- package/dist/providers/accounts/test-accounts.d.ts +12 -0
- package/dist/providers/accounts/test-accounts.js +38 -0
- package/dist/providers/accounts/variables.d.ts +27 -0
- package/dist/providers/accounts/variables.js +94 -0
- package/dist/providers/catalog.d.ts +17 -0
- package/dist/providers/catalog.js +76 -0
- package/dist/providers/claude-models.json +114 -0
- package/dist/providers/claude-provider.d.ts +4 -0
- package/dist/providers/claude-provider.js +29 -0
- package/dist/providers/claude.d.ts +15 -0
- package/dist/providers/claude.js +105 -0
- package/dist/providers/codex-models.json +135 -0
- package/dist/providers/codex-provider.d.ts +4 -0
- package/dist/providers/codex-provider.js +27 -0
- package/dist/providers/codex-rules.d.ts +9 -0
- package/dist/providers/codex-rules.js +35 -0
- package/dist/providers/codex.d.ts +14 -0
- package/dist/providers/codex.js +52 -0
- package/dist/providers/detect.d.ts +6 -0
- package/dist/providers/detect.js +30 -0
- package/dist/providers/provider.d.ts +25 -0
- package/dist/providers/provider.js +1 -0
- package/dist/providers/registry.d.ts +28 -0
- package/dist/providers/registry.js +118 -0
- package/dist/record-directory.d.ts +27 -0
- package/dist/record-directory.js +114 -0
- package/dist/run-process.d.ts +13 -0
- package/dist/run-process.js +42 -0
- package/dist/serializer.d.ts +9 -0
- package/dist/serializer.js +34 -0
- package/dist/skills.d.ts +21 -0
- package/dist/skills.js +174 -0
- package/dist/tasks/background-limit.d.ts +16 -0
- package/dist/tasks/background-limit.js +55 -0
- package/dist/tasks/end-children.d.ts +34 -0
- package/dist/tasks/end-children.js +53 -0
- package/dist/tasks/give-task.d.ts +14 -0
- package/dist/tasks/give-task.js +33 -0
- package/dist/tasks/task-coordinator.d.ts +52 -0
- package/dist/tasks/task-coordinator.js +323 -0
- package/dist/tasks/task-store.d.ts +49 -0
- package/dist/tasks/task-store.js +197 -0
- package/dist/tasks/task-work.d.ts +47 -0
- package/dist/tasks/task-work.js +28 -0
- package/dist/tasks/waiting-child.d.ts +51 -0
- package/dist/tasks/waiting-child.js +143 -0
- package/dist/tasks/wake-parent.d.ts +19 -0
- package/dist/tasks/wake-parent.js +109 -0
- package/dist/tasks/wiring.d.ts +51 -0
- package/dist/tasks/wiring.js +151 -0
- package/dist/title-file.d.ts +2 -0
- package/dist/title-file.js +55 -0
- package/dist/usage/accounts.d.ts +15 -0
- package/dist/usage/accounts.js +64 -0
- package/dist/usage/aggregate.d.ts +12 -0
- package/dist/usage/aggregate.js +154 -0
- package/dist/usage/exchange.d.ts +16 -0
- package/dist/usage/exchange.js +84 -0
- package/dist/usage/index-file.d.ts +15 -0
- package/dist/usage/index-file.js +123 -0
- package/dist/usage/limits/monitor.d.ts +50 -0
- package/dist/usage/limits/monitor.js +179 -0
- package/dist/usage/limits/normalize.d.ts +21 -0
- package/dist/usage/limits/normalize.js +160 -0
- package/dist/usage/limits/probe.d.ts +9 -0
- package/dist/usage/limits/probe.js +113 -0
- package/dist/usage/prices-snapshot.json +1071 -0
- package/dist/usage/pricing.d.ts +34 -0
- package/dist/usage/pricing.js +186 -0
- package/dist/usage/projects.d.ts +17 -0
- package/dist/usage/projects.js +49 -0
- package/dist/usage/readers/claude.d.ts +3 -0
- package/dist/usage/readers/claude.js +59 -0
- package/dist/usage/readers/codex.d.ts +23 -0
- package/dist/usage/readers/codex.js +155 -0
- package/dist/usage/record.d.ts +15 -0
- package/dist/usage/record.js +39 -0
- package/dist/usage/roots.d.ts +12 -0
- package/dist/usage/roots.js +29 -0
- package/dist/usage/scanner.d.ts +30 -0
- package/dist/usage/scanner.js +234 -0
- package/dist/usage/usage-service.d.ts +35 -0
- package/dist/usage/usage-service.js +130 -0
- package/dist/watch-seam.d.ts +29 -0
- package/dist/watch-seam.js +78 -0
- package/dist/watch-test-helpers.d.ts +28 -0
- package/dist/watch-test-helpers.js +86 -0
- package/package.json +42 -3
- package/src/async.ts +40 -0
- package/src/chat/attachment-store.ts +108 -0
- package/src/chat/backend.ts +199 -0
- package/src/chat/background-work.ts +42 -0
- package/src/chat/bookmark-store.ts +166 -0
- package/src/chat/chat-core.ts +1228 -0
- package/src/chat/chat-log.ts +187 -0
- package/src/chat/chat-process.ts +237 -0
- package/src/chat/chat-session.ts +1723 -0
- package/src/chat/chat-store.ts +261 -0
- package/src/chat/chat-title.ts +90 -0
- package/src/chat/claude-backend.ts +235 -0
- package/src/chat/claude-protocol.ts +684 -0
- package/src/chat/claude-title.ts +106 -0
- package/src/chat/claude-transcript.ts +233 -0
- package/src/chat/codex-backend.ts +397 -0
- package/src/chat/codex-protocol.ts +872 -0
- package/src/chat/codex-thread.ts +162 -0
- package/src/chat/codex-transport.ts +224 -0
- package/src/chat/composer-preferences.ts +67 -0
- package/src/chat/context-breakdown.ts +80 -0
- package/src/chat/delta-coalescer.ts +62 -0
- package/src/chat/errors.ts +25 -0
- package/src/chat/fake-claude.ts +1006 -0
- package/src/chat/fake-cli.ts +199 -0
- package/src/chat/fake-codex.ts +534 -0
- package/src/chat/fixtures/claude-2.1.285-auto-fallback.json +24 -0
- package/src/chat/fixtures/claude-2.1.285-native-children.json +6600 -0
- package/src/chat/fixtures/claude-2.1.285-resume-direct-compact.json +202 -0
- package/src/chat/fixtures/claude-2.1.285-resume-task-notification.json +57 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.jsonl +10 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a4c2e8f10b3d5a7e9.meta.json +1 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.jsonl +2 -0
- package/src/chat/fixtures/claude-projects/-work-demo/5f1c2a9e-0b7d-4c1e-9a53-3e2f8d6b7a10/subagents/agent-a9b8c7d6e5f4a3b21.meta.json +1 -0
- package/src/chat/fixtures/codex-0.159.2-native-approval.json +119 -0
- package/src/chat/fixtures/codex-0.159.2-native-file-approval.json +136 -0
- package/src/chat/fixtures/codex-0.159.2-native-interacted.json +220 -0
- package/src/chat/fixtures/codex-0.159.2-usage-windows.json +74 -0
- package/src/chat/input.ts +89 -0
- package/src/chat/limit-resume.ts +74 -0
- package/src/chat/projector.ts +969 -0
- package/src/chat/request-summary.ts +168 -0
- package/src/chat/subagent-projection.ts +45 -0
- package/src/chat/subagent-reader.ts +469 -0
- package/src/chat/subagent-settlement.ts +111 -0
- package/src/chat/thread.ts +167 -0
- package/src/chat/wake-chat.ts +45 -0
- package/src/client-sinks.ts +39 -0
- package/src/coded-error.ts +18 -0
- package/src/context/argv.ts +61 -0
- package/src/context/refusal.ts +50 -0
- package/src/context/verb.ts +392 -0
- package/src/error-text.ts +28 -0
- package/src/events.ts +9 -0
- package/src/fs.ts +118 -0
- package/src/host/agent-host.ts +124 -0
- package/src/host/environment.ts +31 -0
- package/src/host/handlers.ts +142 -0
- package/src/host/memory-port.ts +27 -0
- package/src/host/wiring.ts +164 -0
- package/src/lineage.ts +159 -0
- package/src/messages/deliver-message.ts +61 -0
- package/src/messages/deliver-notice.ts +120 -0
- package/src/messages/notice-notes.ts +34 -0
- package/src/messages/notice-store.ts +142 -0
- package/src/modes.ts +38 -0
- package/src/outbox/manual-clock.ts +31 -0
- package/src/outbox/outbox-worker.ts +214 -0
- package/src/outbox/outbox.ts +106 -0
- package/src/providers/accounts/accounts.ts +192 -0
- package/src/providers/accounts/launch.ts +55 -0
- package/src/providers/accounts/service.ts +632 -0
- package/src/providers/accounts/shadow-home.ts +85 -0
- package/src/providers/accounts/status.ts +106 -0
- package/src/providers/accounts/store.ts +158 -0
- package/src/providers/accounts/test-accounts.ts +48 -0
- package/src/providers/accounts/variables.ts +132 -0
- package/src/providers/catalog.ts +90 -0
- package/src/providers/claude-models.json +114 -0
- package/src/providers/claude-provider.ts +32 -0
- package/src/providers/claude.ts +123 -0
- package/src/providers/codex-models.json +135 -0
- package/src/providers/codex-provider.ts +30 -0
- package/src/providers/codex-rules.ts +54 -0
- package/src/providers/codex.ts +69 -0
- package/src/providers/detect.ts +40 -0
- package/src/providers/provider.ts +46 -0
- package/src/providers/registry.ts +150 -0
- package/src/record-directory.ts +136 -0
- package/src/run-process.ts +61 -0
- package/src/serializer.ts +41 -0
- package/src/skills.ts +199 -0
- package/src/tasks/background-limit.ts +72 -0
- package/src/tasks/end-children.ts +100 -0
- package/src/tasks/give-task.ts +52 -0
- package/src/tasks/task-coordinator.ts +384 -0
- package/src/tasks/task-store.ts +236 -0
- package/src/tasks/task-work.ts +62 -0
- package/src/tasks/waiting-child.ts +203 -0
- package/src/tasks/wake-parent.ts +142 -0
- package/src/tasks/wiring.ts +231 -0
- package/src/title-file.ts +57 -0
- package/src/usage/accounts.ts +86 -0
- package/src/usage/aggregate.ts +220 -0
- package/src/usage/exchange.ts +93 -0
- package/src/usage/index-file.ts +178 -0
- package/src/usage/limits/monitor.ts +239 -0
- package/src/usage/limits/normalize.ts +188 -0
- package/src/usage/limits/probe.ts +120 -0
- package/src/usage/prices-snapshot.json +1071 -0
- package/src/usage/pricing.ts +236 -0
- package/src/usage/projects.ts +65 -0
- package/src/usage/readers/claude.ts +60 -0
- package/src/usage/readers/codex.ts +196 -0
- package/src/usage/record.ts +60 -0
- package/src/usage/roots.ts +39 -0
- package/src/usage/scanner.ts +265 -0
- package/src/usage/usage-service.ts +161 -0
- package/src/watch-seam.ts +111 -0
- package/src/watch-test-helpers.ts +102 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { AgentKind, ProviderCapabilities, ProviderInfo } from '@adecore/agent-contracts';
|
|
2
|
+
import { ModelCatalog } from './catalog.ts';
|
|
3
|
+
import { claudeProvider } from './claude-provider.ts';
|
|
4
|
+
import { codexProvider } from './codex-provider.ts';
|
|
5
|
+
import type { CliDetection } from './detect.ts';
|
|
6
|
+
import type { ChatProvider } from './provider.ts';
|
|
7
|
+
|
|
8
|
+
// How long a "is it installed" answer stays good; an install mid-session shows up on the next check.
|
|
9
|
+
const DETECTION_TTL_MS = 60_000;
|
|
10
|
+
|
|
11
|
+
const DISABLED: CliDetection = { installed: false, version: 'Disabled in Settings' };
|
|
12
|
+
|
|
13
|
+
const NO_CAPABILITIES: ProviderCapabilities = {
|
|
14
|
+
chat: false,
|
|
15
|
+
terminal: false,
|
|
16
|
+
hooks: false,
|
|
17
|
+
streamsToolOutput: false,
|
|
18
|
+
diffs: 'none',
|
|
19
|
+
attachments: false,
|
|
20
|
+
mentions: false,
|
|
21
|
+
denyReason: false,
|
|
22
|
+
allowAlways: false,
|
|
23
|
+
asyncQuestions: false,
|
|
24
|
+
compaction: 'none',
|
|
25
|
+
reportsCost: false,
|
|
26
|
+
reportsContextWindow: false,
|
|
27
|
+
reportsThinking: false,
|
|
28
|
+
slashCommands: false
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/* A CLI of a kind the host does not offer: a record or an account may still name one, and it starts nothing. */
|
|
32
|
+
export function unofferedProvider(kind: AgentKind): ChatProvider {
|
|
33
|
+
return {
|
|
34
|
+
kind,
|
|
35
|
+
name: kind,
|
|
36
|
+
catalog: new ModelCatalog({ defaultModel: '', profiles: {}, models: [] }),
|
|
37
|
+
capabilities: NO_CAPABILITIES,
|
|
38
|
+
command: [kind],
|
|
39
|
+
resumeCommand: '',
|
|
40
|
+
detect: () => Promise.resolve({ installed: false, version: null }),
|
|
41
|
+
firstPromptArgs: () => {
|
|
42
|
+
throw new Error(`${kind} is not offered here`);
|
|
43
|
+
},
|
|
44
|
+
createBackend: () => {
|
|
45
|
+
throw new Error(`${kind} is not offered here`);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const BUILT_IN_PROVIDERS: readonly ChatProvider[] = [claudeProvider, codexProvider];
|
|
51
|
+
|
|
52
|
+
/* The provider of a kind, for the places that have no registry at hand. */
|
|
53
|
+
export function providerFor(kind: AgentKind): ChatProvider {
|
|
54
|
+
return BUILT_IN_PROVIDERS.find((provider) => provider.kind === kind) ?? unofferedProvider(kind);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ProviderRegistryOptions {
|
|
58
|
+
// The CLIs a host offers, in the order every menu lists them; absent, the chat CLIs this package ships.
|
|
59
|
+
providers?: ChatProvider[];
|
|
60
|
+
// The executables to probe, when they are not the ones the providers name.
|
|
61
|
+
commands?: Partial<Record<AgentKind, string>>;
|
|
62
|
+
// How to probe; a test answers without spawning anything.
|
|
63
|
+
detect?: (command: string) => Promise<CliDetection>;
|
|
64
|
+
// The environment a probe runs in.
|
|
65
|
+
env?: Record<string, string | undefined>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* What a host knows about each agent CLI: whether it is there, what it offers and what it can do. */
|
|
69
|
+
export class ProviderRegistry {
|
|
70
|
+
private readonly providers: ChatProvider[];
|
|
71
|
+
private readonly commands: Partial<Record<AgentKind, string>>;
|
|
72
|
+
private readonly detect: ((command: string) => Promise<CliDetection>) | null;
|
|
73
|
+
private readonly env: Record<string, string | undefined>;
|
|
74
|
+
private readonly cache = new Map<AgentKind, { at: number; detection: CliDetection }>();
|
|
75
|
+
|
|
76
|
+
constructor(options: ProviderRegistryOptions = {}) {
|
|
77
|
+
this.providers = options.providers ?? [...BUILT_IN_PROVIDERS];
|
|
78
|
+
this.commands = options.commands ?? {};
|
|
79
|
+
this.detect = options.detect ?? null;
|
|
80
|
+
this.env = options.env ?? process.env;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async list(): Promise<ProviderInfo[]> {
|
|
84
|
+
return await Promise.all(
|
|
85
|
+
this.providers.map(async (provider) => ({
|
|
86
|
+
kind: provider.kind,
|
|
87
|
+
name: provider.name,
|
|
88
|
+
...(await this.detection(provider)),
|
|
89
|
+
models: provider.catalog.list(),
|
|
90
|
+
defaultModel: provider.catalog.defaultModel || null,
|
|
91
|
+
capabilities: provider.capabilities,
|
|
92
|
+
resumeCommand: provider.resumeCommand
|
|
93
|
+
}))
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* The provider of a kind, one this host does not offer included. */
|
|
98
|
+
get(kind: AgentKind): ChatProvider {
|
|
99
|
+
return this.providers.find((provider) => provider.kind === kind) ?? this.fallback(kind);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* The CLI that answers a single prompt here: the one asked for when it is installed, else the
|
|
104
|
+
* first installed provider that can, in the order the catalog lists them. Null on a machine
|
|
105
|
+
* with none, which is what hides the button that would ask.
|
|
106
|
+
*/
|
|
107
|
+
async oneShotProvider(preferred?: AgentKind): Promise<ChatProvider | null> {
|
|
108
|
+
const order = preferred ? [this.get(preferred), ...this.providers] : this.providers;
|
|
109
|
+
for (const provider of order) {
|
|
110
|
+
if (provider.oneShotArgs && (await this.detection(provider)).installed) {
|
|
111
|
+
return provider;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
catalogFor(kind: AgentKind): ModelCatalog {
|
|
118
|
+
return this.get(kind).catalog;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
enabled(kind: AgentKind): boolean {
|
|
122
|
+
return this.get(kind).enabled?.() !== false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
invalidate(kind: AgentKind): void {
|
|
126
|
+
this.cache.delete(kind);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
protected fallback(kind: AgentKind): ChatProvider {
|
|
130
|
+
return unofferedProvider(kind);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async detection(provider: ChatProvider): Promise<CliDetection> {
|
|
134
|
+
if (provider.enabled?.() === false) {
|
|
135
|
+
return DISABLED;
|
|
136
|
+
}
|
|
137
|
+
const cached = this.cache.get(provider.kind);
|
|
138
|
+
if (cached && Date.now() - cached.at < DETECTION_TTL_MS) {
|
|
139
|
+
return cached.detection;
|
|
140
|
+
}
|
|
141
|
+
const command = this.commands[provider.kind] ?? provider.command[0]!;
|
|
142
|
+
const detection = this.detect ? await this.detect(command) : await provider.detect(command, this.env);
|
|
143
|
+
// Turned off while the probe ran.
|
|
144
|
+
if (provider.enabled?.() === false) {
|
|
145
|
+
return DISABLED;
|
|
146
|
+
}
|
|
147
|
+
this.cache.set(provider.kind, { at: Date.now(), detection });
|
|
148
|
+
return detection;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { z } from 'zod';
|
|
4
|
+
import { isNotFound, writeAtomic } from './fs.ts';
|
|
5
|
+
import { KeyedSerializer } from './serializer.ts';
|
|
6
|
+
|
|
7
|
+
// The id is a node id or a chat id a client chose, so it is encoded before it becomes a file name.
|
|
8
|
+
export function recordFileName(id: string): string {
|
|
9
|
+
return `${encodeURIComponent(id)}.json`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RecordDirectoryOptions<T> {
|
|
13
|
+
dir: string;
|
|
14
|
+
schema: z.ZodType<T>;
|
|
15
|
+
idOf(record: T): string;
|
|
16
|
+
write?: (path: string, text: string) => Promise<void>;
|
|
17
|
+
/*
|
|
18
|
+
* What a record read from disk is worth keeping as, for a store that drops what went stale while
|
|
19
|
+
* the host was down. Null takes the file with it.
|
|
20
|
+
*/
|
|
21
|
+
keep?(record: T): T | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* One kind of record under a host's data folder, a file per record and the whole of it in memory. A file
|
|
26
|
+
* that does not parse is left alone rather than deleted: it is a record of an older or newer host,
|
|
27
|
+
* and the one thing worse than not reading it is throwing it away.
|
|
28
|
+
*/
|
|
29
|
+
export class RecordDirectory<T> {
|
|
30
|
+
readonly dir: string;
|
|
31
|
+
private readonly schema: z.ZodType<T>;
|
|
32
|
+
private readonly idOf: (record: T) => string;
|
|
33
|
+
private readonly writeFile: (path: string, text: string) => Promise<void>;
|
|
34
|
+
private readonly keep: ((record: T) => T | null) | null;
|
|
35
|
+
private readonly records = new Map<string, T>();
|
|
36
|
+
// One write at a time per record, so an older one never lands after a newer one.
|
|
37
|
+
private readonly writes = new KeyedSerializer();
|
|
38
|
+
|
|
39
|
+
constructor(options: RecordDirectoryOptions<T>) {
|
|
40
|
+
this.dir = options.dir;
|
|
41
|
+
this.schema = options.schema;
|
|
42
|
+
this.idOf = options.idOf;
|
|
43
|
+
this.writeFile = options.write ?? writeAtomic;
|
|
44
|
+
this.keep = options.keep ?? null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Reads what an earlier run of the host wrote down. Call before anything can ask. */
|
|
48
|
+
async load(): Promise<void> {
|
|
49
|
+
let names: string[];
|
|
50
|
+
try {
|
|
51
|
+
names = await readdir(this.dir);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
if (isNotFound(e)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
throw e;
|
|
57
|
+
}
|
|
58
|
+
for (const name of names) {
|
|
59
|
+
if (!name.endsWith('.json')) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const raw = await readFile(join(this.dir, name), 'utf8').catch(() => null);
|
|
63
|
+
if (raw === null) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
let parsed: ReturnType<typeof this.schema.safeParse>;
|
|
67
|
+
try {
|
|
68
|
+
parsed = this.schema.safeParse(JSON.parse(raw));
|
|
69
|
+
} catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!parsed.success) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const record = this.keep === null ? parsed.data : this.keep(parsed.data);
|
|
76
|
+
if (record === null) {
|
|
77
|
+
await rm(join(this.dir, name), { force: true });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
this.records.set(this.idOf(record), record);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get(id: string): T | undefined {
|
|
85
|
+
return this.records.get(id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
has(id: string): boolean {
|
|
89
|
+
return this.records.has(id);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
all(): T[] {
|
|
93
|
+
return [...this.records.values()];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
* Holds the record in memory at once, so the next question in the same tick already sees it, and
|
|
98
|
+
* answers whether it is still the one this directory holds by the time the file is written. A
|
|
99
|
+
* record removed while its file was being written does not come back: the rename is undone.
|
|
100
|
+
*/
|
|
101
|
+
write(record: T): Promise<boolean> {
|
|
102
|
+
const id = this.idOf(record);
|
|
103
|
+
this.records.set(id, record);
|
|
104
|
+
return this.writes.run(id, async () => {
|
|
105
|
+
if (this.records.get(id) !== record) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
await mkdir(this.dir, { recursive: true, mode: 0o700 });
|
|
109
|
+
await this.writeFile(this.pathOf(id), JSON.stringify(record));
|
|
110
|
+
if (!this.records.has(id)) {
|
|
111
|
+
await rm(this.pathOf(id), { force: true });
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Forgets a record here and now; its file follows. */
|
|
119
|
+
remove(id: string): Promise<void> {
|
|
120
|
+
this.records.delete(id);
|
|
121
|
+
return rm(this.pathOf(id), { force: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Drops every record the predicate says is gone. */
|
|
125
|
+
async prune(drop: (record: T) => boolean): Promise<void> {
|
|
126
|
+
for (const record of this.all()) {
|
|
127
|
+
if (drop(record)) {
|
|
128
|
+
await this.remove(this.idOf(record));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private pathOf(id: string): string {
|
|
134
|
+
return join(this.dir, recordFileName(id));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export interface ProcessResult {
|
|
4
|
+
// Null when the process ended on a signal.
|
|
5
|
+
exitCode: number | null;
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RunProcessOptions {
|
|
11
|
+
env?: Record<string, string | undefined>;
|
|
12
|
+
cwd?: string;
|
|
13
|
+
// Written and closed at once; absent, the process reads from nothing.
|
|
14
|
+
stdin?: string;
|
|
15
|
+
// The process is sent a SIGTERM after this, and the run fails.
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
timeoutMessage?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* One short command to its end, with what it printed. A command that cannot start rejects. */
|
|
21
|
+
export function runProcess(command: readonly string[], options: RunProcessOptions = {}): Promise<ProcessResult> {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const [executable, ...args] = command;
|
|
24
|
+
if (executable === undefined) {
|
|
25
|
+
reject(new Error('No command to run'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const child = spawn(executable, args, {
|
|
29
|
+
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
30
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
31
|
+
stdio: [options.stdin === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe']
|
|
32
|
+
});
|
|
33
|
+
const stdout: Buffer[] = [];
|
|
34
|
+
const stderr: Buffer[] = [];
|
|
35
|
+
child.stdout?.on('data', (chunk: Buffer) => stdout.push(chunk));
|
|
36
|
+
child.stderr?.on('data', (chunk: Buffer) => stderr.push(chunk));
|
|
37
|
+
const timer =
|
|
38
|
+
options.timeoutMs === undefined
|
|
39
|
+
? null
|
|
40
|
+
: setTimeout(() => {
|
|
41
|
+
child.kill('SIGTERM');
|
|
42
|
+
reject(new Error(options.timeoutMessage ?? `${executable} did not finish in time`));
|
|
43
|
+
}, options.timeoutMs);
|
|
44
|
+
child.on('error', (error) => {
|
|
45
|
+
if (timer !== null) {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
reject(error);
|
|
49
|
+
});
|
|
50
|
+
child.on('close', (exitCode) => {
|
|
51
|
+
if (timer !== null) {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
resolve({ exitCode, stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8') });
|
|
55
|
+
});
|
|
56
|
+
if (options.stdin !== undefined && child.stdin) {
|
|
57
|
+
child.stdin.on('error', () => undefined);
|
|
58
|
+
child.stdin.end(options.stdin);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* One writer at a time: the next piece of work starts once the one before it settled, whether that
|
|
3
|
+
* one kept its promise or not, and every caller hears its own result. Two changes in a row never
|
|
4
|
+
* read the same file and drop one of them.
|
|
5
|
+
*/
|
|
6
|
+
export class Serializer {
|
|
7
|
+
private chain: Promise<unknown> = Promise.resolve();
|
|
8
|
+
|
|
9
|
+
run<T>(work: () => Promise<T>): Promise<T> {
|
|
10
|
+
const next = this.chain.then(work, work);
|
|
11
|
+
this.chain = next.catch(() => undefined);
|
|
12
|
+
return next;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Resolves once what is queued has settled, without adding work of its own. */
|
|
16
|
+
idle(): Promise<void> {
|
|
17
|
+
return this.chain.then(
|
|
18
|
+
() => undefined,
|
|
19
|
+
() => undefined
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* The same per key, for a store whose records are written one by one and never wait on each other. */
|
|
25
|
+
export class KeyedSerializer {
|
|
26
|
+
private readonly chains = new Map<string, Promise<unknown>>();
|
|
27
|
+
|
|
28
|
+
run<T>(key: string, work: () => Promise<T>): Promise<T> {
|
|
29
|
+
const previous = this.chains.get(key) ?? Promise.resolve();
|
|
30
|
+
const next = previous.then(work, work);
|
|
31
|
+
const settled = next.catch(() => undefined);
|
|
32
|
+
this.chains.set(key, settled);
|
|
33
|
+
// A key whose chain ran out leaves nothing behind, or the map grows with every id ever written.
|
|
34
|
+
void settled.then(() => {
|
|
35
|
+
if (this.chains.get(key) === settled) {
|
|
36
|
+
this.chains.delete(key);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return next;
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/skills.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { existsSync, type Dirent } from 'node:fs';
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, parse } from 'node:path';
|
|
5
|
+
import type { AgentKind, ChatSkill, ChatSkillSource } from '@adecore/agent-contracts';
|
|
6
|
+
import { isNotFound } from './fs.ts';
|
|
7
|
+
|
|
8
|
+
// One folder full of skill folders, and what the CLI calls the skills it finds there.
|
|
9
|
+
export interface SkillRoot {
|
|
10
|
+
dir: string;
|
|
11
|
+
source: ChatSkillSource;
|
|
12
|
+
// A plugin names its skills `<plugin>:<skill>`, the way the CLI's init frame does.
|
|
13
|
+
prefix?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SkillRootOptions {
|
|
17
|
+
// The person's home directory; a test points it at a temporary tree.
|
|
18
|
+
home?: string;
|
|
19
|
+
// Where Claude Code keeps its own configuration, when it is not `<home>/.claude`.
|
|
20
|
+
claudeConfigDir?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const NAME = /^[A-Za-z][A-Za-z0-9_:-]*$/;
|
|
24
|
+
|
|
25
|
+
function unquote(value: string): string {
|
|
26
|
+
const quoted = /^(['"])([\s\S]*)\1$/.exec(value);
|
|
27
|
+
return quoted ? quoted[2]! : value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* The `key: value` pairs of a SKILL.md front matter. Enough YAML for what a skill file holds: flat
|
|
32
|
+
* keys, quoted or bare values, and the block scalars (`>-`, `|`) a long description is written as.
|
|
33
|
+
*/
|
|
34
|
+
export function parseFrontmatter(text: string): Record<string, string> {
|
|
35
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
36
|
+
if (!match) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
const fields: Record<string, string> = {};
|
|
40
|
+
const lines = match[1]!.split(/\r?\n/);
|
|
41
|
+
for (let i = 0; i < lines.length; i++) {
|
|
42
|
+
const pair = /^([A-Za-z0-9_-]+):[ \t]*(.*)$/.exec(lines[i]!);
|
|
43
|
+
if (!pair) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
let value = pair[2]!.trim();
|
|
47
|
+
if (value === '' || value === '>' || value === '>-' || value === '|' || value === '|-') {
|
|
48
|
+
// A block scalar puts the value on the indented lines under it; `|` keeps the newlines.
|
|
49
|
+
const keepNewlines = value.startsWith('|');
|
|
50
|
+
const parts: string[] = [];
|
|
51
|
+
while (i + 1 < lines.length && /^[ \t]+\S/.test(lines[i + 1]!)) {
|
|
52
|
+
parts.push(lines[++i]!.trim());
|
|
53
|
+
}
|
|
54
|
+
value = parts.join(keepNewlines ? '\n' : ' ');
|
|
55
|
+
}
|
|
56
|
+
fields[pair[1]!] = unquote(value);
|
|
57
|
+
}
|
|
58
|
+
return fields;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* The folders from `cwd` up to and including the repository root, closest first. The home folder
|
|
63
|
+
* itself is not one of them, since its `.claude/skills` is already the user root.
|
|
64
|
+
*/
|
|
65
|
+
function projectDirs(cwd: string, home: string): string[] {
|
|
66
|
+
const dirs: string[] = [];
|
|
67
|
+
let current = cwd;
|
|
68
|
+
while (current !== '' && current !== parse(current).root && current !== home && current !== dirname(home) && dirs.length < 8) {
|
|
69
|
+
dirs.push(current);
|
|
70
|
+
// Anything above a repository root belongs to another project, so the walk ends there.
|
|
71
|
+
if (existsSync(join(current, '.git'))) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
const parent = dirname(current);
|
|
75
|
+
if (parent === current) {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
current = parent;
|
|
79
|
+
}
|
|
80
|
+
return dirs;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* The plugin folders Claude Code installed, each named the way its skills are addressed. */
|
|
84
|
+
export async function pluginSkillRoots(configDir: string): Promise<SkillRoot[]> {
|
|
85
|
+
let raw: string;
|
|
86
|
+
try {
|
|
87
|
+
raw = await readFile(join(configDir, 'plugins', 'installed_plugins.json'), 'utf8');
|
|
88
|
+
} catch (e) {
|
|
89
|
+
if (isNotFound(e)) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
throw e;
|
|
93
|
+
}
|
|
94
|
+
let parsed: unknown;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(raw);
|
|
97
|
+
} catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const plugins = typeof parsed === 'object' && parsed !== null ? (parsed as { plugins?: unknown }).plugins : undefined;
|
|
101
|
+
if (typeof plugins !== 'object' || plugins === null) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const roots: SkillRoot[] = [];
|
|
105
|
+
for (const [key, installs] of Object.entries(plugins as Record<string, unknown>)) {
|
|
106
|
+
const name = key.split('@')[0] ?? key;
|
|
107
|
+
for (const install of Array.isArray(installs) ? installs : []) {
|
|
108
|
+
const path = typeof install === 'object' && install !== null ? (install as { installPath?: unknown }).installPath : undefined;
|
|
109
|
+
if (typeof path === 'string' && path !== '') {
|
|
110
|
+
roots.push({ dir: join(path, 'skills'), source: 'plugin', prefix: name });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return roots;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Where a provider's CLI looks for skills, in the order a name clash is decided. */
|
|
118
|
+
export async function skillRootsFor(kind: AgentKind, cwd: string, options: SkillRootOptions = {}): Promise<SkillRoot[]> {
|
|
119
|
+
const home = options.home ?? homedir();
|
|
120
|
+
if (kind === 'codex') {
|
|
121
|
+
return [
|
|
122
|
+
{ dir: join(home, '.agents', 'skills'), source: 'user' },
|
|
123
|
+
{ dir: join(home, '.codex', 'skills'), source: 'user' },
|
|
124
|
+
...projectDirs(cwd, home).map((dir): SkillRoot => ({ dir: join(dir, '.agents', 'skills'), source: 'project' }))
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
const configDir = options.claudeConfigDir ?? join(home, '.claude');
|
|
128
|
+
return [
|
|
129
|
+
{ dir: join(configDir, 'skills'), source: 'user' },
|
|
130
|
+
...projectDirs(cwd, home).map((dir): SkillRoot => ({ dir: join(dir, '.claude', 'skills'), source: 'project' })),
|
|
131
|
+
...(await pluginSkillRoots(configDir))
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function readSkill(root: SkillRoot, folder: string): Promise<ChatSkill | null> {
|
|
136
|
+
let text: string;
|
|
137
|
+
try {
|
|
138
|
+
text = await readFile(join(root.dir, folder, 'SKILL.md'), 'utf8');
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const fields = parseFrontmatter(text);
|
|
143
|
+
const own = fields.name && NAME.test(fields.name) ? fields.name : folder;
|
|
144
|
+
const name = root.prefix ? `${root.prefix}:${own}` : own;
|
|
145
|
+
if (!NAME.test(name)) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return { name, description: fields.description ?? '', source: root.source };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Every skill under these roots, sorted by name; the first root that carries a name wins. */
|
|
152
|
+
export async function scanSkillRoots(roots: SkillRoot[]): Promise<ChatSkill[]> {
|
|
153
|
+
const found = new Map<string, ChatSkill>();
|
|
154
|
+
for (const root of roots) {
|
|
155
|
+
let entries: Dirent[];
|
|
156
|
+
try {
|
|
157
|
+
entries = await readdir(root.dir, { withFileTypes: true });
|
|
158
|
+
} catch {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
// A skill manager links each skill in from a shared folder; a link to anything but a skill folder reads as no skill.
|
|
162
|
+
const folders = entries.filter((entry) => entry.isDirectory() || entry.isSymbolicLink());
|
|
163
|
+
const skills = await Promise.all(folders.map((entry) => readSkill(root, entry.name)));
|
|
164
|
+
for (const skill of skills) {
|
|
165
|
+
if (skill && !found.has(skill.name)) {
|
|
166
|
+
found.set(skill.name, skill);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function discoverSkills(kind: AgentKind, cwd: string, options: SkillRootOptions = {}): Promise<ChatSkill[]> {
|
|
174
|
+
return scanSkillRoots(await skillRootsFor(kind, cwd, options));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// How long a scan stays good; a skill written mid-session shows up on the next look.
|
|
178
|
+
const CACHE_TTL_MS = 30_000;
|
|
179
|
+
|
|
180
|
+
/* The skills of one CLI in one folder, scanned at most once per half minute. There is no watcher. */
|
|
181
|
+
export class SkillIndex {
|
|
182
|
+
private readonly options: SkillRootOptions;
|
|
183
|
+
private readonly cache = new Map<string, { at: number; skills: ChatSkill[] }>();
|
|
184
|
+
|
|
185
|
+
constructor(options: SkillRootOptions = {}) {
|
|
186
|
+
this.options = options;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async list(kind: AgentKind, cwd: string): Promise<ChatSkill[]> {
|
|
190
|
+
const key = `${kind}:${cwd}`;
|
|
191
|
+
const cached = this.cache.get(key);
|
|
192
|
+
if (cached && Date.now() - cached.at < CACHE_TTL_MS) {
|
|
193
|
+
return cached.skills;
|
|
194
|
+
}
|
|
195
|
+
const skills = await discoverSkills(kind, cwd, this.options);
|
|
196
|
+
this.cache.set(key, { at: Date.now(), skills });
|
|
197
|
+
return skills;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { commandLabel, runningInBackground } from '../chat/background-work.ts';
|
|
2
|
+
import { loadChat, type ChatOpenerDeps } from '../chat/wake-chat.ts';
|
|
3
|
+
import type { OutboxOutcome } from '../outbox/outbox-worker.ts';
|
|
4
|
+
import type { TaskCoordinator, TaskCoordinatorDeps } from './task-coordinator.ts';
|
|
5
|
+
import type { TaskStore } from './task-store.ts';
|
|
6
|
+
import { isBackgroundLimit, type AnyOutboxEntry, type BackgroundLimitEntry, type TaskOutbox } from './task-work.ts';
|
|
7
|
+
|
|
8
|
+
export interface BackgroundLimitDeps extends ChatOpenerDeps {
|
|
9
|
+
tasks: Pick<TaskStore, 'get'>;
|
|
10
|
+
coordinator: Pick<TaskCoordinator, 'outlasted'>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* The limit on a child's background commands passed, or a restart took them down. Its task settles on
|
|
15
|
+
* the child's last turn (failed after a restart), unless something else settled it first or a subagent
|
|
16
|
+
* or a workflow now holds it, which has no limit; a turn in flight may still answer it, so the limit
|
|
17
|
+
* looks again once that turn ended.
|
|
18
|
+
*/
|
|
19
|
+
export function backgroundLimitHandler(deps: BackgroundLimitDeps) {
|
|
20
|
+
return async (entry: BackgroundLimitEntry): Promise<OutboxOutcome> => {
|
|
21
|
+
if (deps.tasks.get(entry.payload.taskId)?.status !== 'open') {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const chat = await loadChat(deps, entry.target);
|
|
25
|
+
if (chat === null) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (chat.info.activeTurnId !== null) {
|
|
29
|
+
return 'wait';
|
|
30
|
+
}
|
|
31
|
+
if (runningInBackground(chat.thread.list()).length > 0) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// A child whose process went since the limit started runs nothing any more; what it ran then is what the parent is told.
|
|
35
|
+
const running = (chat.info.background ?? []).map(commandLabel);
|
|
36
|
+
await deps.coordinator.outlasted(entry.target, running.length > 0 ? running : entry.payload.commands, entry.payload.restarted ? 'restart' : 'limit');
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RestartedOutbox {
|
|
41
|
+
list(): readonly AnyOutboxEntry[];
|
|
42
|
+
update(entry: BackgroundLimitEntry): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* Every limit still owed when the host starts was owed by an earlier run, and the CLI whose commands
|
|
47
|
+
* it waits on went down with that run, so nothing is left to wait for: each is due now and fails its
|
|
48
|
+
* task. Call after the outbox loaded and before its worker starts.
|
|
49
|
+
*/
|
|
50
|
+
export async function restartBackgroundLimits(outbox: RestartedOutbox, now: number): Promise<void> {
|
|
51
|
+
for (const entry of outbox.list()) {
|
|
52
|
+
if (isBackgroundLimit(entry)) {
|
|
53
|
+
await outbox.update({ ...entry, notBefore: now, payload: { ...entry.payload, restarted: true } });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* The `background-limit` entries of a host's outbox, one per task at most, as the coordinator owes and drops them. */
|
|
59
|
+
export function backgroundLimits(outbox: Pick<TaskOutbox, 'list' | 'enqueue' | 'remove'>): TaskCoordinatorDeps['limit'] {
|
|
60
|
+
return {
|
|
61
|
+
owed: (taskId) => outbox.list().some((entry) => isBackgroundLimit(entry) && entry.payload.taskId === taskId),
|
|
62
|
+
owe: (task, commands, at) =>
|
|
63
|
+
outbox.enqueue(task.projectId, task.childId, { kind: 'background-limit', payload: { taskId: task.id, commands: [...commands] } }, at),
|
|
64
|
+
lapse: async (taskId) => {
|
|
65
|
+
for (const entry of outbox.list()) {
|
|
66
|
+
if (isBackgroundLimit(entry) && entry.payload.taskId === taskId) {
|
|
67
|
+
await outbox.remove(entry.id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|