@gajae-code/coding-agent 0.16.7 → 0.17.1
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/CHANGELOG.md +180 -1
- package/dist/types/capability/fs.d.ts +37 -4
- package/dist/types/capability/index.d.ts +19 -0
- package/dist/types/capability/types.d.ts +9 -0
- package/dist/types/cli/doctor/args.d.ts +36 -0
- package/dist/types/cli/doctor/checks.d.ts +18 -0
- package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
- package/dist/types/cli/doctor/context.d.ts +89 -0
- package/dist/types/cli/doctor/files.d.ts +27 -0
- package/dist/types/cli/doctor/ids.d.ts +20 -0
- package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
- package/dist/types/cli/doctor/journal.d.ts +43 -0
- package/dist/types/cli/doctor/managed-link.d.ts +39 -0
- package/dist/types/cli/doctor/native-probe.d.ts +2 -0
- package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
- package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
- package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
- package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
- package/dist/types/cli/doctor/probe-client.d.ts +9 -0
- package/dist/types/cli/doctor/probe-types.d.ts +18 -0
- package/dist/types/cli/doctor/probe.d.ts +3 -0
- package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
- package/dist/types/cli/doctor/repairs.d.ts +28 -0
- package/dist/types/cli/doctor/report.d.ts +27 -0
- package/dist/types/cli/doctor/runner.d.ts +4 -0
- package/dist/types/cli/doctor/safety.d.ts +10 -0
- package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
- package/dist/types/cli/doctor/service-targets.d.ts +41 -0
- package/dist/types/cli/doctor/types.d.ts +145 -0
- package/dist/types/cli/doctor-cli.d.ts +7 -0
- package/dist/types/cli/doctor-supervisor.d.ts +121 -0
- package/dist/types/cli/doctor-worker.d.ts +29 -0
- package/dist/types/cli/install-activation.d.ts +158 -0
- package/dist/types/cli/macos-community-app.d.ts +41 -0
- package/dist/types/cli/mcp-cli.d.ts +0 -11
- package/dist/types/cli/update-cli.d.ts +41 -4
- package/dist/types/cli-main.d.ts +174 -0
- package/dist/types/cli.d.ts +1 -171
- package/dist/types/commands/doctor.d.ts +7 -0
- package/dist/types/commands/sdk.d.ts +17 -8
- package/dist/types/config/autorouting-tier-map.d.ts +19 -0
- package/dist/types/config/file-lock.d.ts +27 -3
- package/dist/types/config/model-preset-registry.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +12 -5
- package/dist/types/config/model-resolver.d.ts +16 -3
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
- package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
- package/dist/types/cursor.d.ts +9 -9
- package/dist/types/discovery/agents-md.d.ts +6 -1
- package/dist/types/discovery/helpers.d.ts +36 -7
- package/dist/types/discovery/index.d.ts +1 -1
- package/dist/types/discovery/mcp-json.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +22 -1
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/plugins/manager.d.ts +33 -4
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
- package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
- package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
- package/dist/types/main.d.ts +19 -1
- package/dist/types/modes/acp/acp-agent.d.ts +40 -0
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
- package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -2
- package/dist/types/sdk/acp/adapter.d.ts +5 -1
- package/dist/types/sdk/broker/broker.d.ts +51 -0
- package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
- package/dist/types/sdk/broker/discovery.d.ts +38 -0
- package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
- package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
- package/dist/types/sdk/broker/runtime.d.ts +26 -0
- package/dist/types/sdk/broker/session-index.d.ts +13 -4
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
- package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
- package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
- package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/bus/notification-service.d.ts +3 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
- package/dist/types/sdk/cli/session-cli.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/host/surface-policy.d.ts +3 -1
- package/dist/types/sdk/lifecycle/service.d.ts +44 -10
- package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
- package/dist/types/sdk/prompt-failure.d.ts +47 -0
- package/dist/types/sdk/prompt-status.d.ts +19 -0
- package/dist/types/sdk/router/session-router.d.ts +3 -4
- package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
- package/dist/types/sdk/session-authority.d.ts +9 -0
- package/dist/types/sdk/session.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +1 -1
- package/dist/types/session/fallback-chain-controller.d.ts +3 -6
- package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
- package/dist/types/session/primary-control-surface.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +6 -0
- package/dist/types/tools/ask.d.ts +25 -0
- package/dist/types/tools/image-gen.d.ts +0 -4
- package/dist/types/tools/skill-discovery.d.ts +7 -5
- package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
- package/dist/types/utils/changelog.d.ts +0 -9
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +9 -8
- package/scripts/compile-args.ts +8 -4
- package/src/capability/fs.ts +395 -17
- package/src/capability/index.ts +273 -10
- package/src/capability/types.ts +9 -0
- package/src/cli/customize-doctor.ts +18 -12
- package/src/cli/doctor/args.ts +236 -0
- package/src/cli/doctor/checks.ts +431 -0
- package/src/cli/doctor/config-repairs.ts +517 -0
- package/src/cli/doctor/context.ts +149 -0
- package/src/cli/doctor/files.ts +149 -0
- package/src/cli/doctor/ids.ts +115 -0
- package/src/cli/doctor/install-repairs.ts +335 -0
- package/src/cli/doctor/journal.ts +423 -0
- package/src/cli/doctor/managed-link.ts +514 -0
- package/src/cli/doctor/native-probe.ts +13 -0
- package/src/cli/doctor/permission-repairs.ts +242 -0
- package/src/cli/doctor/plugin-quarantine.ts +377 -0
- package/src/cli/doctor/plugin-restore.ts +59 -0
- package/src/cli/doctor/plugin-targets.ts +373 -0
- package/src/cli/doctor/probe-client.ts +195 -0
- package/src/cli/doctor/probe-types.ts +21 -0
- package/src/cli/doctor/probe.ts +42 -0
- package/src/cli/doctor/projection-probe.ts +249 -0
- package/src/cli/doctor/repairs.ts +1157 -0
- package/src/cli/doctor/report.ts +176 -0
- package/src/cli/doctor/runner.ts +342 -0
- package/src/cli/doctor/safety.ts +40 -0
- package/src/cli/doctor/service-repairs.ts +452 -0
- package/src/cli/doctor/service-targets.ts +274 -0
- package/src/cli/doctor/types.ts +164 -0
- package/src/cli/doctor-cli.ts +62 -0
- package/src/cli/doctor-supervisor.ts +571 -0
- package/src/cli/doctor-worker.ts +181 -0
- package/src/cli/install-activation.ts +666 -0
- package/src/cli/macos-community-app.ts +1449 -0
- package/src/cli/mcp-cli.ts +11 -21
- package/src/cli/nofile-limit.ts +5 -1
- package/src/cli/plugin-cli.ts +6 -1
- package/src/cli/update-cli.ts +362 -264
- package/src/cli-main.ts +517 -0
- package/src/cli.ts +64 -433
- package/src/commands/autoresearch.ts +11 -5
- package/src/commands/completion.ts +1 -1
- package/src/commands/doctor.ts +13 -0
- package/src/commands/sdk.ts +58 -10
- package/src/commands/ultragoal.ts +11 -5
- package/src/config/autorouting-tier-map.ts +7 -0
- package/src/config/file-lock.ts +736 -71
- package/src/config/mcp-schema.json +3 -3
- package/src/config/model-preset-registry.ts +1 -0
- package/src/config/model-registry.ts +80 -27
- package/src/config/model-resolver.ts +33 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -0
- package/src/coordinator-mcp/server.ts +534 -51
- package/src/coordinator-mcp/session-reaper.ts +64 -8
- package/src/cursor.ts +223 -53
- package/src/discovery/agents-md.ts +74 -8
- package/src/discovery/agents.ts +32 -7
- package/src/discovery/builtin.ts +184 -75
- package/src/discovery/claude-plugins.ts +85 -17
- package/src/discovery/cline.ts +38 -10
- package/src/discovery/cursor.ts +19 -6
- package/src/discovery/gemini.ts +62 -22
- package/src/discovery/github.ts +13 -3
- package/src/discovery/helpers.ts +483 -89
- package/src/discovery/index.ts +1 -0
- package/src/discovery/mcp-json.ts +36 -10
- package/src/discovery/opencode.ts +36 -19
- package/src/discovery/ssh.ts +11 -8
- package/src/discovery/windsurf.ts +12 -3
- package/src/exec/bash-executor.ts +10 -3
- package/src/exec/isolated-shell.ts +15 -2
- package/src/extensibility/extensions/loader.ts +88 -26
- package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
- package/src/extensibility/gjc-plugins/paths.ts +4 -1
- package/src/extensibility/gjc-plugins/types.ts +27 -0
- package/src/extensibility/plugins/manager.ts +200 -22
- package/src/extensibility/plugins/marketplace/cache.ts +55 -2
- package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
- package/src/extensibility/plugins/marketplace/registry.ts +153 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
- package/src/extensibility/runtime-skill-discovery.ts +61 -9
- package/src/extensibility/skill-management.ts +7 -1
- package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
- package/src/gjc-runtime/launch-worktree.test.ts +248 -0
- package/src/gjc-runtime/launch-worktree.ts +275 -38
- package/src/gjc-runtime/repository-binding.ts +26 -1
- package/src/gjc-runtime/session-layout.ts +10 -0
- package/src/gjc-runtime/session-state-lock.ts +330 -23
- package/src/gjc-runtime/session-state-sidecar.ts +328 -80
- package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +28 -26
- package/src/main.ts +150 -29
- package/src/memories/index.ts +24 -2
- package/src/modes/acp/acp-agent.ts +522 -68
- package/src/modes/components/agent-dashboard.ts +3 -1
- package/src/modes/components/model-selector.ts +28 -19
- package/src/modes/components/plugin-settings.ts +2 -1
- package/src/modes/controllers/selector-controller.ts +15 -9
- package/src/modes/interactive-mode.ts +21 -3
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/move-session.md +4 -0
- package/src/prompts/tools/read.md +2 -1
- package/src/prompts/tools/skill-discovery.md +6 -5
- package/src/prompts/tools/task.md +1 -0
- package/src/runtime-mcp/config-writer.ts +228 -0
- package/src/runtime-mcp/oauth-discovery.ts +114 -14
- package/src/runtime-mcp/policy-json-edit.ts +129 -0
- package/src/runtime-mcp/startup-policy.ts +14 -0
- package/src/runtime-mcp/types.ts +2 -2
- package/src/sdk/acp/adapter.ts +27 -12
- package/src/sdk/broker/broker.ts +273 -44
- package/src/sdk/broker/daemon-entry.ts +119 -0
- package/src/sdk/broker/discovery.ts +173 -20
- package/src/sdk/broker/doctor-restart.ts +206 -0
- package/src/sdk/broker/endpoint-authority.ts +36 -0
- package/src/sdk/broker/ensure.ts +49 -14
- package/src/sdk/broker/lifecycle.ts +174 -42
- package/src/sdk/broker/process-incarnation.ts +86 -2
- package/src/sdk/broker/runtime.ts +145 -21
- package/src/sdk/broker/session-index.ts +136 -22
- package/src/sdk/broker/spawn-substrate.ts +4 -1
- package/src/sdk/broker/transport.ts +5 -0
- package/src/sdk/broker/worktree-occupancy.ts +21 -1
- package/src/sdk/bus/chat-daemon-cli.ts +63 -0
- package/src/sdk/bus/chat-daemon-control.ts +260 -64
- package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
- package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
- package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
- package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
- package/src/sdk/bus/index.ts +252 -51
- package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
- package/src/sdk/bus/notification-service.ts +21 -3
- package/src/sdk/bus/reconciliation-store.ts +17 -11
- package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +47 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
- package/src/sdk/bus/telegram-daemon.ts +178 -13
- package/src/sdk/cli/session-cli.ts +100 -12
- package/src/sdk/host/host.ts +2 -7
- package/src/sdk/host/session-runtime.test.ts +329 -8
- package/src/sdk/host/session-runtime.ts +202 -50
- package/src/sdk/host/surface-policy.ts +8 -1
- package/src/sdk/lifecycle/service.ts +199 -22
- package/src/sdk/mcp/server.ts +1 -1
- package/src/sdk/prompt-deadline-lease.ts +12 -4
- package/src/sdk/prompt-deadline-manager.ts +5 -7
- package/src/sdk/prompt-failure.ts +148 -0
- package/src/sdk/prompt-status.ts +30 -1
- package/src/sdk/prompt-watchdog.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +20 -8
- package/src/sdk/router/session-router.ts +8 -3
- package/src/sdk/service-artifact-paths.ts +26 -0
- package/src/sdk/session-authority.ts +10 -0
- package/src/sdk/session.ts +361 -194
- package/src/session/agent-session.ts +398 -93
- package/src/session/blob-store.ts +9 -9
- package/src/session/fallback-chain-controller.ts +3 -7
- package/src/session/internal/managed-session-scope.ts +445 -29
- package/src/session/internal/managed-session-storage.ts +38 -6
- package/src/session/primary-control-surface.ts +12 -0
- package/src/session/session-manager.ts +11 -3
- package/src/session/session-storage.ts +1 -1
- package/src/session-import/codex.ts +6 -1
- package/src/session-import/redact.ts +7 -0
- package/src/skill-state/workflow-mutation-guard.ts +645 -39
- package/src/slash-commands/builtin-registry.ts +7 -3
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +36 -3
- package/src/task/types.ts +27 -5
- package/src/tools/ask.ts +114 -34
- package/src/tools/browser/actions.ts +2 -2
- package/src/tools/browser/tab-worker.ts +11 -8
- package/src/tools/descriptors.ts +6 -1
- package/src/tools/fetch.ts +7 -2
- package/src/tools/image-gen.ts +33 -34
- package/src/tools/read.ts +78 -32
- package/src/tools/skill-discovery.ts +17 -7
- package/src/tools/tool-catalog.generated.ts +8 -8
- package/src/tools/ultragoal-ask-guard.ts +16 -26
- package/src/utils/changelog.ts +7 -1
- package/src/utils/git.ts +3 -1
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- package/src/web/search/providers/openai-compatible.ts +191 -14
|
@@ -17,6 +17,8 @@ import * as path from "node:path";
|
|
|
17
17
|
|
|
18
18
|
import { getConfigRootDir, getPluginsDir, isEnoent, logger, tryParseJson } from "@gajae-code/utils";
|
|
19
19
|
|
|
20
|
+
import { withFileLock } from "../../../config/file-lock";
|
|
21
|
+
|
|
20
22
|
import type {
|
|
21
23
|
InstalledPluginEntry,
|
|
22
24
|
InstalledPluginsRegistry,
|
|
@@ -131,6 +133,157 @@ export async function writeInstalledPluginsRegistry(filePath: string, reg: Insta
|
|
|
131
133
|
await atomicWriteJson(filePath, reg);
|
|
132
134
|
}
|
|
133
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Refuse a registry file that is not an ordinary, singly-linked regular file:
|
|
138
|
+
* a symlink could redirect the write elsewhere, and a hard link (nlink > 1)
|
|
139
|
+
* means the mutation would silently also change another path. Absence is not
|
|
140
|
+
* a refusal — that is the normal "nothing installed yet" state.
|
|
141
|
+
*/
|
|
142
|
+
async function assertOrdinaryRegistryFile(filePath: string): Promise<void> {
|
|
143
|
+
let stat: import("node:fs").Stats;
|
|
144
|
+
try {
|
|
145
|
+
stat = await fs.lstat(filePath);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (isEnoent(err)) return;
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink > 1) {
|
|
151
|
+
throw Object.assign(new Error(`Installed plugins registry at ${filePath} is not a safe ordinary file`), {
|
|
152
|
+
code: "unsafe_registry_link",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Strict read for mutation/CAS callers: a missing file is legitimately empty,
|
|
159
|
+
* but malformed JSON or an unexpected shape must never be silently treated as
|
|
160
|
+
* an empty registry — that would let a mutating writer overwrite (and lose)
|
|
161
|
+
* an unreadable but non-empty registry. Use {@link readInstalledPluginsRegistry}
|
|
162
|
+
* for lenient, display-only reads.
|
|
163
|
+
*/
|
|
164
|
+
async function readInstalledPluginsRegistryStrict(filePath: string): Promise<InstalledPluginsRegistry> {
|
|
165
|
+
await assertOrdinaryRegistryFile(filePath);
|
|
166
|
+
let text: string;
|
|
167
|
+
try {
|
|
168
|
+
text = await fs.readFile(filePath, "utf8");
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (isEnoent(err)) return emptyInstalledPluginsRegistry();
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
const data = tryParseJson<InstalledPluginsRegistry>(text);
|
|
174
|
+
if (
|
|
175
|
+
!data ||
|
|
176
|
+
typeof data !== "object" ||
|
|
177
|
+
typeof data.version !== "number" ||
|
|
178
|
+
!data.plugins ||
|
|
179
|
+
typeof data.plugins !== "object" ||
|
|
180
|
+
Array.isArray(data.plugins)
|
|
181
|
+
) {
|
|
182
|
+
throw Object.assign(new Error(`Installed plugins registry is malformed at ${filePath}`), {
|
|
183
|
+
code: "malformed_registry",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return { ...data, version: 2 };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** dev/ino/mtime/size identity of a file plus its parent directory, for exact CAS. */
|
|
190
|
+
async function fileParentIdentity(filePath: string): Promise<Record<string, string> | null> {
|
|
191
|
+
try {
|
|
192
|
+
const stat = await fs.lstat(filePath, { bigint: true });
|
|
193
|
+
const parent = await fs.lstat(path.dirname(filePath), { bigint: true });
|
|
194
|
+
return {
|
|
195
|
+
dev: stat.dev.toString(),
|
|
196
|
+
ino: stat.ino.toString(),
|
|
197
|
+
mtimeNs: stat.mtimeNs.toString(),
|
|
198
|
+
size: stat.size.toString(),
|
|
199
|
+
parentDev: parent.dev.toString(),
|
|
200
|
+
parentIno: parent.ino.toString(),
|
|
201
|
+
};
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function entryBaseline(identity: Record<string, string> | null, entry: InstalledPluginEntry): string {
|
|
208
|
+
return JSON.stringify({ identity, entry });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export type InstalledPluginEnablementState =
|
|
212
|
+
| { status: "ok"; enabled: boolean; baseline: string }
|
|
213
|
+
| { status: "not_installed" }
|
|
214
|
+
| { status: "malformed" }
|
|
215
|
+
| { status: "unsafe_link" };
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Pure read-only snapshot of one (id, scope) entry's durable enablement plus
|
|
219
|
+
* an exact file+parent CAS baseline. Never touches the cached artifact at
|
|
220
|
+
* `installPath` — presence/absence of that artifact is a D6 concern, not D7's.
|
|
221
|
+
* A symlinked or hard-linked registry file is refused distinctly from an
|
|
222
|
+
* absent ("not_installed"-eligible) or malformed one.
|
|
223
|
+
*/
|
|
224
|
+
export async function getInstalledPluginEnablementState(
|
|
225
|
+
filePath: string,
|
|
226
|
+
id: string,
|
|
227
|
+
scope: "user" | "project",
|
|
228
|
+
): Promise<InstalledPluginEnablementState> {
|
|
229
|
+
let reg: InstalledPluginsRegistry;
|
|
230
|
+
try {
|
|
231
|
+
reg = await readInstalledPluginsRegistryStrict(filePath);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error instanceof Error && (error as { code?: string }).code === "unsafe_registry_link")
|
|
234
|
+
return { status: "unsafe_link" };
|
|
235
|
+
return { status: "malformed" };
|
|
236
|
+
}
|
|
237
|
+
const entry = reg.plugins[id]?.find(e => e.scope === scope);
|
|
238
|
+
if (!entry) return { status: "not_installed" };
|
|
239
|
+
const identity = await fileParentIdentity(filePath);
|
|
240
|
+
return { status: "ok", enabled: entry.enabled !== false, baseline: entryBaseline(identity, entry) };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Update durable marketplace enablement without touching cached artifacts.
|
|
245
|
+
* Serialized under the canonical native-identity-bound file lock ordinary
|
|
246
|
+
* marketplace writers use (bounded retries, no blind directory removal).
|
|
247
|
+
* `expectedBaseline` is required: a caller with no prior read has no CAS
|
|
248
|
+
* authority and must call {@link getInstalledPluginEnablementState} first.
|
|
249
|
+
*/
|
|
250
|
+
export async function setInstalledPluginEnabled(
|
|
251
|
+
filePath: string,
|
|
252
|
+
id: string,
|
|
253
|
+
scope: "user" | "project",
|
|
254
|
+
enabled: boolean,
|
|
255
|
+
expectedBaseline: string,
|
|
256
|
+
): Promise<"updated" | "not_needed"> {
|
|
257
|
+
return await withFileLock(filePath, async () => {
|
|
258
|
+
let reg: InstalledPluginsRegistry;
|
|
259
|
+
try {
|
|
260
|
+
reg = await readInstalledPluginsRegistryStrict(filePath);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (error instanceof Error && (error as { code?: string }).code === "unsafe_registry_link") throw error;
|
|
263
|
+
throw Object.assign(new Error(`Installed plugins registry is malformed at ${filePath}`), {
|
|
264
|
+
code: "malformed_registry",
|
|
265
|
+
cause: error,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const entries = reg.plugins[id];
|
|
269
|
+
const index = entries?.findIndex(entry => entry.scope === scope) ?? -1;
|
|
270
|
+
if (!entries || index < 0)
|
|
271
|
+
throw Object.assign(new Error(`Plugin "${id}" is not installed in ${scope} scope`), { code: "not_installed" });
|
|
272
|
+
const current = entries[index] as InstalledPluginEntry;
|
|
273
|
+
const identity = await fileParentIdentity(filePath);
|
|
274
|
+
const baseline = entryBaseline(identity, current);
|
|
275
|
+
if (baseline !== expectedBaseline)
|
|
276
|
+
throw Object.assign(new Error("The installed plugin entry changed since it was reviewed"), {
|
|
277
|
+
code: "stale_baseline",
|
|
278
|
+
});
|
|
279
|
+
if ((current.enabled ?? true) === enabled) return "not_needed";
|
|
280
|
+
const nextEntries = [...entries];
|
|
281
|
+
nextEntries[index] = { ...current, enabled };
|
|
282
|
+
await writeInstalledPluginsRegistry(filePath, { ...reg, plugins: { ...reg.plugins, [id]: nextEntries } });
|
|
283
|
+
return "updated";
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
134
287
|
// ── Marketplace CRUD ─────────────────────────────────────────────────
|
|
135
288
|
// Pure functions that transform registry state. Caller is responsible for
|
|
136
289
|
// reading, mutating, and writing back.
|
|
@@ -27,6 +27,34 @@ export interface ResolveContext {
|
|
|
27
27
|
tmpDir: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function sourcePin(source: PluginSource): { ref?: string; sha?: string; immutable: boolean } {
|
|
31
|
+
if (typeof source === "string") return { immutable: false };
|
|
32
|
+
if (source.source === "npm") return { immutable: false };
|
|
33
|
+
return {
|
|
34
|
+
ref: source.ref,
|
|
35
|
+
sha: source.sha,
|
|
36
|
+
immutable: typeof source.sha === "string" && /^[a-f0-9]{40}$/i.test(source.sha),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function assertPinnedSource(source: PluginSource): void {
|
|
41
|
+
const pin = sourcePin(source);
|
|
42
|
+
if (!pin.immutable) throw new Error("marketplace restore requires an immutable source SHA");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Independently verify that a resolved git checkout's HEAD matches the pinned
|
|
47
|
+
* SHA the plan/token claims to restore. `git.clone` already checks out the
|
|
48
|
+
* exact SHA, but this re-derives the fact from the checkout itself (a fresh
|
|
49
|
+
* `rev-parse HEAD` in the resolved directory) rather than trusting whatever
|
|
50
|
+
* the resolver returned, so a resolver bug or a swapped working tree cannot
|
|
51
|
+
* silently mismatch source and to-be-published bytes.
|
|
52
|
+
*/
|
|
53
|
+
export async function verifyResolvedProvenance(resolvedDir: string, expectedSha: string): Promise<boolean> {
|
|
54
|
+
const actual = await git.head.sha(resolvedDir).catch(() => null);
|
|
55
|
+
return typeof actual === "string" && actual.toLowerCase() === expectedSha.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
30
58
|
/**
|
|
31
59
|
* Resolve a plugin source to an absolute local directory path.
|
|
32
60
|
*
|
|
@@ -65,12 +93,10 @@ async function resolveRelativeSource(
|
|
|
65
93
|
|
|
66
94
|
// Resolve against marketplace root (not the .Anthropic model-plugin/ catalog subdirectory)
|
|
67
95
|
const resolved = path.resolve(context.marketplaceClonePath, relativePath);
|
|
96
|
+
const outsideRoot = `Plugin source "${source}" resolves outside marketplace root ("${context.marketplaceClonePath}")`;
|
|
68
97
|
|
|
69
|
-
if (!pathIsWithin(context.marketplaceClonePath, resolved))
|
|
70
|
-
|
|
71
|
-
`Plugin source "${source}" resolves outside marketplace root ("${context.marketplaceClonePath}")`,
|
|
72
|
-
);
|
|
73
|
-
}
|
|
98
|
+
if (!pathIsWithin(context.marketplaceClonePath, resolved)) throw new Error(outsideRoot);
|
|
99
|
+
await assertCanonicalContainment(context.marketplaceClonePath, resolved, outsideRoot);
|
|
74
100
|
|
|
75
101
|
await verifyDirExists(resolved, `Plugin source directory does not exist: "${resolved}"`);
|
|
76
102
|
return { dir: resolved };
|
|
@@ -87,13 +113,13 @@ async function resolveObjectSource(
|
|
|
87
113
|
// { source: "url", url: "https://github.com/owner/repo.git" }
|
|
88
114
|
// Despite the name, this is typically a git clone URL
|
|
89
115
|
const targetDir = path.join(context.tmpDir, `plugin-${crypto.randomUUID()}`);
|
|
90
|
-
await git.clone(source.url, targetDir, { ref: source.ref, sha: source.sha });
|
|
116
|
+
await git.clone(assertCloneUrl(source.url), targetDir, { ref: source.ref, sha: source.sha });
|
|
91
117
|
return { dir: targetDir, tempCloneRoot: targetDir };
|
|
92
118
|
}
|
|
93
119
|
|
|
94
120
|
case "github": {
|
|
95
121
|
// { source: "github", repo: "owner/repo" }
|
|
96
|
-
const url = `https://github.com/${source.repo}.git`;
|
|
122
|
+
const url = `https://github.com/${assertRepoSlug(source.repo)}.git`;
|
|
97
123
|
const targetDir = path.join(context.tmpDir, `plugin-${crypto.randomUUID()}`);
|
|
98
124
|
await git.clone(url, targetDir, { ref: source.ref, sha: source.sha });
|
|
99
125
|
return { dir: targetDir, tempCloneRoot: targetDir };
|
|
@@ -103,17 +129,20 @@ async function resolveObjectSource(
|
|
|
103
129
|
// { source: "git-subdir", url: "owner/repo" | "https://...", path: "plugins/foo" }
|
|
104
130
|
const url =
|
|
105
131
|
source.url.includes("://") || source.url.startsWith("git@")
|
|
106
|
-
? source.url
|
|
107
|
-
: `https://github.com/${source.url}.git`;
|
|
132
|
+
? assertCloneUrl(source.url)
|
|
133
|
+
: `https://github.com/${assertRepoSlug(source.url)}.git`;
|
|
134
|
+
assertSubdirPath(source.path);
|
|
108
135
|
const cloneDir = path.join(context.tmpDir, `plugin-repo-${crypto.randomUUID()}`);
|
|
109
136
|
await git.clone(url, cloneDir, { ref: source.ref, sha: source.sha });
|
|
110
137
|
|
|
111
138
|
const subdirPath = path.resolve(cloneDir, source.path);
|
|
139
|
+
const escapes = `git-subdir path "${source.path}" escapes the cloned repository`;
|
|
112
140
|
if (!pathIsWithin(cloneDir, subdirPath)) {
|
|
113
141
|
await fs.rm(cloneDir, { recursive: true, force: true });
|
|
114
|
-
throw new Error(
|
|
142
|
+
throw new Error(escapes);
|
|
115
143
|
}
|
|
116
144
|
try {
|
|
145
|
+
await assertCanonicalContainment(cloneDir, subdirPath, escapes);
|
|
117
146
|
await verifyDirExists(subdirPath, `git-subdir path "${source.path}" does not exist in cloned repository`);
|
|
118
147
|
} catch (err) {
|
|
119
148
|
await fs.rm(cloneDir, { recursive: true, force: true });
|
|
@@ -132,6 +161,83 @@ async function resolveObjectSource(
|
|
|
132
161
|
|
|
133
162
|
// ── Helpers ─────────────────────────────────────────────────────────
|
|
134
163
|
|
|
164
|
+
/** Network transports a plugin source may be fetched over. */
|
|
165
|
+
const CLONE_URL_SCHEME = /^(?:https|http|git|ssh|file):\/\//;
|
|
166
|
+
/** scp-like `user@host:path`, which git accepts without a scheme. */
|
|
167
|
+
const SCP_LIKE_URL = /^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:/;
|
|
168
|
+
/** `git-remote-<helper>` dispatch (`ext::sh -c ...`), i.e. arbitrary command execution. */
|
|
169
|
+
const REMOTE_HELPER_URL = /^[A-Za-z0-9][A-Za-z0-9+.-]*::/;
|
|
170
|
+
/** `owner/repo` as GitHub itself accepts it; no options, no path traversal, no separators. */
|
|
171
|
+
const REPO_SLUG = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A catalog is untrusted input, and its URL goes straight to `git clone`.
|
|
175
|
+
*
|
|
176
|
+
* Two shapes are dangerous on their own: a value starting with `-`, which git
|
|
177
|
+
* reads as an option (`--upload-pack=<cmd>` is command execution), and a
|
|
178
|
+
* remote-helper URL such as `ext::sh -c ...`, which git dispatches to
|
|
179
|
+
* `git-remote-ext` and which executes its argument by design. `git.clone` also
|
|
180
|
+
* passes `--` before the URL, but that only closes the first hole and only for
|
|
181
|
+
* callers that go through it, so both are refused here where the failure can
|
|
182
|
+
* name the offending catalog entry.
|
|
183
|
+
*
|
|
184
|
+
* Local absolute paths stay allowed: a privately hosted marketplace pointing at
|
|
185
|
+
* an on-disk repository is a supported layout.
|
|
186
|
+
*/
|
|
187
|
+
function assertCloneUrl(url: string): string {
|
|
188
|
+
const supported =
|
|
189
|
+
!url.startsWith("-") &&
|
|
190
|
+
!REMOTE_HELPER_URL.test(url) &&
|
|
191
|
+
(CLONE_URL_SCHEME.test(url) || SCP_LIKE_URL.test(url) || path.isAbsolute(url));
|
|
192
|
+
if (!supported)
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Plugin source URL must be an https/http/git/ssh/file URL, a user@host:path remote, or an absolute local path — got: "${url}"`,
|
|
195
|
+
);
|
|
196
|
+
return url;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function assertRepoSlug(repo: string): string {
|
|
200
|
+
if (!REPO_SLUG.test(repo)) throw new Error(`Plugin source repository must be "owner/repo" — got: "${repo}"`);
|
|
201
|
+
return repo;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The subdir is joined into a clone path, so it must be a plain relative path. */
|
|
205
|
+
function assertSubdirPath(subdir: string): string {
|
|
206
|
+
if (subdir.startsWith("-") || path.isAbsolute(subdir) || path.win32.isAbsolute(subdir))
|
|
207
|
+
throw new Error(`git-subdir path must be a relative path — got: "${subdir}"`);
|
|
208
|
+
return subdir;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Decide containment on canonical paths, and fail closed when they cannot be
|
|
213
|
+
* derived.
|
|
214
|
+
*
|
|
215
|
+
* A catalog is untrusted input and controls the tree it ships, so the plugin
|
|
216
|
+
* directory it names can be a symlink pointing out of the marketplace root.
|
|
217
|
+
* `pathIsWithin` does resolve symlinks, but it silently degrades to a lexical
|
|
218
|
+
* comparison whenever `realpath` fails — a dangling or looping link therefore
|
|
219
|
+
* passes the containment gate and is only rejected later (or not at all) by the
|
|
220
|
+
* existence probe, which follows symlinks itself. Here both sides must
|
|
221
|
+
* canonicalize successfully. A candidate that exists as a directory entry but
|
|
222
|
+
* cannot be canonicalized (dangling or looping link) is classified as an
|
|
223
|
+
* untrusted source; a path that is simply absent is left to the existence probe
|
|
224
|
+
* so a missing plugin still reports as missing.
|
|
225
|
+
*/
|
|
226
|
+
async function assertCanonicalContainment(root: string, candidate: string, errorMessage: string): Promise<void> {
|
|
227
|
+
const canonicalRoot = await fs.realpath(root).catch(() => undefined);
|
|
228
|
+
if (canonicalRoot === undefined) throw new Error(errorMessage);
|
|
229
|
+
const canonicalCandidate = await fs.realpath(candidate).catch(() => undefined);
|
|
230
|
+
if (canonicalCandidate === undefined) {
|
|
231
|
+
const entryExists = await fs
|
|
232
|
+
.lstat(candidate)
|
|
233
|
+
.then(() => true)
|
|
234
|
+
.catch(() => false);
|
|
235
|
+
if (entryExists) throw new Error(errorMessage);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (!pathIsWithin(canonicalRoot, canonicalCandidate)) throw new Error(errorMessage);
|
|
239
|
+
}
|
|
240
|
+
|
|
135
241
|
async function verifyDirExists(dirPath: string, errorMessage: string): Promise<void> {
|
|
136
242
|
try {
|
|
137
243
|
const stat = await fs.stat(dirPath);
|
|
@@ -10,6 +10,7 @@ import { findRepoRoot } from "../capability/fs";
|
|
|
10
10
|
import type { Skill as CapabilitySkill } from "../capability/skill";
|
|
11
11
|
import type { SkillsSettings } from "../config/settings-schema";
|
|
12
12
|
import { resolveSkillScopeTrust } from "../config/skill-settings-defaults";
|
|
13
|
+
import { BUNDLED_GJC_SKILL_CATALOG } from "../defaults/gjc-skills.generated";
|
|
13
14
|
import { scanClaudeProjectSkills, scanClaudeUserSkills } from "../discovery/claude";
|
|
14
15
|
import { loadMarketplaceSkills } from "../discovery/claude-plugins";
|
|
15
16
|
import { scanCodexProjectSkills, scanCodexUserSkills } from "../discovery/codex";
|
|
@@ -27,7 +28,9 @@ import {
|
|
|
27
28
|
import { expandTilde } from "../tools/path-utils";
|
|
28
29
|
import { loadSkills, type Skill } from "./skills";
|
|
29
30
|
|
|
30
|
-
export type RuntimeSkillDiscoverySource = "project" | "user";
|
|
31
|
+
export type RuntimeSkillDiscoverySource = "project" | "user" | "bundled";
|
|
32
|
+
/** Search requests cover filesystem scopes; bundled is only a candidate source. */
|
|
33
|
+
type RuntimeSkillDiscoveryScope = Exclude<RuntimeSkillDiscoverySource, "bundled">;
|
|
31
34
|
|
|
32
35
|
export interface RuntimeSkillDiscoveryCandidate {
|
|
33
36
|
name: string;
|
|
@@ -49,6 +52,8 @@ export interface RuntimeSkillDiscoveryDiagnostics {
|
|
|
49
52
|
|
|
50
53
|
export interface RuntimeSkillDiscoveryResult {
|
|
51
54
|
candidates: RuntimeSkillDiscoveryCandidate[];
|
|
55
|
+
/** Deduped skill metadata the query filter ran against, including bundled entries; candidates.length <= scanned. */
|
|
56
|
+
scanned: number;
|
|
52
57
|
diagnostics: RuntimeSkillDiscoveryDiagnostics;
|
|
53
58
|
}
|
|
54
59
|
|
|
@@ -60,7 +65,7 @@ export interface DiscoverRuntimeSkillsOptions {
|
|
|
60
65
|
profileAuthority?: "default" | "custom";
|
|
61
66
|
query?: string;
|
|
62
67
|
limit?: number;
|
|
63
|
-
source?:
|
|
68
|
+
source?: RuntimeSkillDiscoveryScope | "all";
|
|
64
69
|
policy?: SkillsSettings;
|
|
65
70
|
}
|
|
66
71
|
|
|
@@ -146,7 +151,7 @@ function getUseWhen(skill: CapabilitySkill): string[] | undefined {
|
|
|
146
151
|
return values.length > 0 ? values : undefined;
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
function sourceEnabled(source:
|
|
154
|
+
function sourceEnabled(source: RuntimeSkillDiscoveryScope, policy: SkillsSettings | undefined): boolean {
|
|
150
155
|
if (policy?.enabled !== true) return false;
|
|
151
156
|
return resolveSkillScopeTrust(policy, source);
|
|
152
157
|
}
|
|
@@ -184,7 +189,7 @@ async function realPathOrSelf(filePath: string): Promise<string> {
|
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
interface ScanJobResult {
|
|
187
|
-
items: Array<{ skill: CapabilitySkill; source:
|
|
192
|
+
items: Array<{ skill: CapabilitySkill; source: RuntimeSkillDiscoveryScope; providerPriority: number }>;
|
|
188
193
|
warnings: string[];
|
|
189
194
|
label: string;
|
|
190
195
|
}
|
|
@@ -261,12 +266,13 @@ async function scanProjectOrUserDir(
|
|
|
261
266
|
dir: string,
|
|
262
267
|
level: "project" | "user",
|
|
263
268
|
label: string,
|
|
264
|
-
source:
|
|
269
|
+
source: RuntimeSkillDiscoveryScope,
|
|
265
270
|
providerPriority: number,
|
|
266
271
|
authorityRoot?: string,
|
|
267
272
|
): Promise<ScanJobResult> {
|
|
268
273
|
const result = await scanSkillsFromDir(ctx, {
|
|
269
274
|
dir,
|
|
275
|
+
linkContainmentRoot: level === "project" ? (ctx.repoRoot ?? undefined) : undefined,
|
|
270
276
|
authorityRoot,
|
|
271
277
|
providerId: "runtime",
|
|
272
278
|
level,
|
|
@@ -282,7 +288,7 @@ async function scanProjectOrUserDir(
|
|
|
282
288
|
interface ConventionImportScan {
|
|
283
289
|
host: "Claude Code" | "Codex";
|
|
284
290
|
dir: string;
|
|
285
|
-
scope:
|
|
291
|
+
scope: RuntimeSkillDiscoveryScope;
|
|
286
292
|
skills: CapabilitySkill[];
|
|
287
293
|
}
|
|
288
294
|
|
|
@@ -297,7 +303,7 @@ interface ConventionImportScan {
|
|
|
297
303
|
*/
|
|
298
304
|
async function collectConventionImportCandidates(
|
|
299
305
|
ctx: { cwd: string; home: string; repoRoot: string | null },
|
|
300
|
-
source:
|
|
306
|
+
source: RuntimeSkillDiscoveryScope | "all",
|
|
301
307
|
policy: SkillsSettings | undefined,
|
|
302
308
|
): Promise<ConventionImportScan[]> {
|
|
303
309
|
const scans: ConventionImportScan[] = [];
|
|
@@ -359,7 +365,7 @@ function reportConventionImportCandidates(
|
|
|
359
365
|
* tool and `gjc skills discover`.
|
|
360
366
|
*/
|
|
361
367
|
export function describeDisabledSkillScopes(
|
|
362
|
-
source:
|
|
368
|
+
source: RuntimeSkillDiscoveryScope | "all",
|
|
363
369
|
policy: SkillsSettings | undefined,
|
|
364
370
|
): string | undefined {
|
|
365
371
|
if (policy?.enabled !== true) {
|
|
@@ -379,6 +385,36 @@ export function describeDisabledSkillScopes(
|
|
|
379
385
|
return `Skill discovery skipped disabled scope(s): ${skipped.join(", ")}. Enable them with ${commands.join(" and ")}.`;
|
|
380
386
|
}
|
|
381
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Explain a zero-candidate result caused by the conjunctive query filter rather
|
|
390
|
+
* than by an empty catalog or disabled scopes: every whitespace-separated term
|
|
391
|
+
* must appear in a candidate's name, description, source, or use conditions (or
|
|
392
|
+
* one term must equal the exact skill name), so a single keyword that appears
|
|
393
|
+
* nowhere drops every skill. Shared by the skill_discovery tool.
|
|
394
|
+
*/
|
|
395
|
+
export function describeNoSkillMatch(query: string | undefined, scanned: number): string | undefined {
|
|
396
|
+
const trimmed = (query ?? "").trim();
|
|
397
|
+
if (trimmed.length === 0 || scanned === 0) return undefined;
|
|
398
|
+
return `No skill matched every query term (${scanned} skill${scanned === 1 ? "" : "s"} scanned). Matching is conjunctive substring: every whitespace-separated term must appear in the skill's name, description, source, or use conditions, unless a term equals the exact skill name. Retry with the exact skill name or fewer terms; a zero-candidate result does not prove no skills exist.`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function getBundledSkillCandidates(): RuntimeSkillDiscoveryCandidate[] {
|
|
402
|
+
// Catalog loaders remain lazy; discovery reads only each entry's metadata.
|
|
403
|
+
return BUNDLED_GJC_SKILL_CATALOG.flatMap(entry => {
|
|
404
|
+
if (entry.kind !== "skill" || entry.name === undefined) return [];
|
|
405
|
+
return [
|
|
406
|
+
{
|
|
407
|
+
name: entry.name,
|
|
408
|
+
description: entry.description ?? "",
|
|
409
|
+
source: "bundled" as const,
|
|
410
|
+
// `embedded:gjc/...` is the stable non-filesystem identifier used by
|
|
411
|
+
// bundled Skill metadata; the relative path comes from the generated catalog.
|
|
412
|
+
path: `embedded:gjc/${entry.relativePath}`,
|
|
413
|
+
},
|
|
414
|
+
];
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
382
418
|
export async function discoverRuntimeSkills(
|
|
383
419
|
options: DiscoverRuntimeSkillsOptions,
|
|
384
420
|
): Promise<RuntimeSkillDiscoveryResult> {
|
|
@@ -442,7 +478,7 @@ export async function discoverRuntimeSkills(
|
|
|
442
478
|
collectPluginSkills(home, options.cwd, allowedLevels).then(result => ({
|
|
443
479
|
items: result.items.map(skill => ({
|
|
444
480
|
skill,
|
|
445
|
-
source: skill.level as
|
|
481
|
+
source: skill.level as RuntimeSkillDiscoveryScope,
|
|
446
482
|
providerPriority: 70,
|
|
447
483
|
})),
|
|
448
484
|
warnings: result.warnings,
|
|
@@ -461,6 +497,20 @@ export async function discoverRuntimeSkills(
|
|
|
461
497
|
const seenNames = new Set<string>();
|
|
462
498
|
const seenPaths = new Set<string>();
|
|
463
499
|
const candidates: RuntimeSkillDiscoveryCandidate[] = [];
|
|
500
|
+
let scanned = 0;
|
|
501
|
+
// Bundled workflow skills are neither project nor user scope, so explicit
|
|
502
|
+
// project/user requests exclude them; source "all" includes them regardless
|
|
503
|
+
// of filesystem trust and include/ignore/disabled policy filters because the
|
|
504
|
+
// bundled definitions cannot be replaced.
|
|
505
|
+
if (source === "all") {
|
|
506
|
+
for (const candidate of getBundledSkillCandidates()) {
|
|
507
|
+
const normalizedName = getSkillFilesystemIdentity(candidate.name);
|
|
508
|
+
seenNames.add(normalizedName);
|
|
509
|
+
seenPaths.add(candidate.path);
|
|
510
|
+
scanned += 1;
|
|
511
|
+
if (matchesQuery(candidate, options.query ?? "")) candidates.push(candidate);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
464
514
|
const orderedItems: ScanJobResult["items"] = [];
|
|
465
515
|
for (const entry of settled) {
|
|
466
516
|
if ("error" in entry) {
|
|
@@ -499,6 +549,7 @@ export async function discoverRuntimeSkills(
|
|
|
499
549
|
path: item.skill.path,
|
|
500
550
|
useWhen: getUseWhen(item.skill),
|
|
501
551
|
};
|
|
552
|
+
scanned += 1;
|
|
502
553
|
if (matchesQuery(candidate, options.query ?? "")) candidates.push(candidate);
|
|
503
554
|
}
|
|
504
555
|
candidates.sort((a, b) => compareSkillOrder(a.name, a.path, b.name, b.path));
|
|
@@ -509,6 +560,7 @@ export async function discoverRuntimeSkills(
|
|
|
509
560
|
);
|
|
510
561
|
return {
|
|
511
562
|
candidates: candidates.slice(0, normalizeLimit(options.limit)),
|
|
563
|
+
scanned,
|
|
512
564
|
diagnostics: { messages: diagnostics },
|
|
513
565
|
};
|
|
514
566
|
}
|
|
@@ -329,7 +329,13 @@ export async function listNativeSkillsForManagement(options: {
|
|
|
329
329
|
scanJobs.push(
|
|
330
330
|
scanSkillsFromDir(
|
|
331
331
|
{ cwd: options.cwd, home, repoRoot: projectDirs.repoRoot },
|
|
332
|
-
{
|
|
332
|
+
{
|
|
333
|
+
dir,
|
|
334
|
+
linkContainmentRoot: projectDirs.repoRoot ?? undefined,
|
|
335
|
+
providerId: "runtime",
|
|
336
|
+
level: "project",
|
|
337
|
+
requireDescription: true,
|
|
338
|
+
},
|
|
333
339
|
).then(result => ({ dir, scope: "project" as const, items: result.items })),
|
|
334
340
|
);
|
|
335
341
|
}
|
|
@@ -894,6 +894,15 @@ export async function autoresearchIssueVerdict(input: {
|
|
|
894
894
|
|
|
895
895
|
/* ------------------------------ CLI dispatch ------------------------------ */
|
|
896
896
|
|
|
897
|
+
/**
|
|
898
|
+
* A help request is `--help`/`-h` anywhere in the argv, or a leading `help`
|
|
899
|
+
* verb. Exported so the CLI command class routes help back through this
|
|
900
|
+
* runtime instead of a generic command-level fallback.
|
|
901
|
+
*/
|
|
902
|
+
export function isAutoresearchHelpInvocation(args: readonly string[]): boolean {
|
|
903
|
+
return args.includes("--help") || args.includes("-h") || args[0] === "help";
|
|
904
|
+
}
|
|
905
|
+
|
|
897
906
|
function renderAutoresearchHelp(): string {
|
|
898
907
|
return [
|
|
899
908
|
"Run native GJC Autoresearch workflow commands",
|
|
@@ -1081,7 +1090,7 @@ export async function runNativeAutoresearchCommand(
|
|
|
1081
1090
|
cwd = process.cwd(),
|
|
1082
1091
|
): Promise<AutoresearchCommandResult> {
|
|
1083
1092
|
try {
|
|
1084
|
-
if (
|
|
1093
|
+
if (isAutoresearchHelpInvocation(args)) {
|
|
1085
1094
|
return { status: 0, stdout: renderAutoresearchHelp() };
|
|
1086
1095
|
}
|
|
1087
1096
|
const verb = args[0];
|