@gajae-code/coding-agent 0.16.3 → 0.16.6
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 +113 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/customize-doctor.d.ts +22 -0
- package/dist/types/cli/root-flags.d.ts +4 -2
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +31 -3
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +7 -0
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +35 -0
- package/dist/types/exec/bash-executor.d.ts +17 -1
- package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
- package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-signals.d.ts +2 -0
- package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
- package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
- package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
- package/dist/types/exec/bash-shell-worker.d.ts +1 -0
- package/dist/types/exec/isolated-shell.d.ts +19 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
- package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/runner.d.ts +46 -6
- package/dist/types/extensibility/extensions/types.d.ts +16 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/extensibility/skill-management.d.ts +9 -4
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
- package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/bash-execution.d.ts +1 -0
- package/dist/types/modes/components/execution-shared.d.ts +1 -0
- package/dist/types/modes/components/settings-selector.d.ts +6 -1
- package/dist/types/modes/components/status-line/command.d.ts +35 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -0
- package/dist/types/modes/components/tool-status-header.d.ts +14 -0
- package/dist/types/modes/controllers/event-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/runtime-mcp/client.d.ts +9 -1
- package/dist/types/runtime-mcp/manager.d.ts +7 -1
- package/dist/types/runtime-mcp/pool.d.ts +22 -3
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/runtime-mcp/types.d.ts +22 -0
- package/dist/types/sdk/broker/ensure.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
- package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
- package/dist/types/sdk/client/client.d.ts +6 -0
- package/dist/types/sdk/host/host.d.ts +3 -1
- package/dist/types/sdk/host/session-runtime.d.ts +26 -1
- package/dist/types/sdk/lifecycle/service.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +10 -2
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/session.d.ts +5 -1
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/setup/paseo/announce.d.ts +112 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/skill-state/canonical-skills.d.ts +4 -0
- package/dist/types/system-prompt.d.ts +10 -0
- package/dist/types/task/executor.d.ts +5 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/github-cache.d.ts +2 -1
- package/dist/types/tools/index.d.ts +10 -0
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/package.json +9 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +11 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/capability/index.ts +14 -2
- package/src/capability/types.ts +4 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/customize-doctor.ts +155 -12
- package/src/cli/root-flags.ts +5 -3
- package/src/cli.ts +22 -0
- package/src/commands/sdk.ts +74 -31
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock-gc.ts +40 -2
- package/src/config/file-lock.ts +379 -31
- package/src/config/model-preset-registry.ts +120 -19
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +73 -23
- package/src/config/model-resolver.ts +48 -0
- package/src/config/settings-schema.ts +33 -1
- package/src/config/settings.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +33 -2
- package/src/coordinator-mcp/server.ts +477 -101
- package/src/crash/sanitize.ts +1 -1
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
- package/src/discovery/builtin.ts +52 -55
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +307 -34
- package/src/exec/bash-executor.ts +330 -50
- package/src/exec/bash-shell-guardian.ts +388 -0
- package/src/exec/bash-shell-runtime-entry.ts +5 -0
- package/src/exec/bash-shell-signals.ts +29 -0
- package/src/exec/bash-shell-supervisor-entry.ts +3 -0
- package/src/exec/bash-shell-supervisor.ts +359 -0
- package/src/exec/bash-shell-worker-entry.ts +5 -0
- package/src/exec/bash-shell-worker-protocol.ts +36 -0
- package/src/exec/bash-shell-worker.ts +116 -0
- package/src/exec/isolated-shell.ts +393 -0
- package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
- package/src/extensibility/extensions/function-hooks.ts +959 -0
- package/src/extensibility/extensions/index.ts +22 -0
- package/src/extensibility/extensions/loader.ts +53 -2
- package/src/extensibility/extensions/runner.ts +895 -264
- package/src/extensibility/extensions/types.ts +42 -0
- package/src/extensibility/extensions/wrapper.ts +56 -15
- package/src/extensibility/gjc-plugins/compiler.ts +228 -60
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
- package/src/extensibility/gjc-plugins/installer.ts +13 -12
- package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
- package/src/extensibility/gjc-plugins/paths.ts +13 -0
- package/src/extensibility/gjc-plugins/registry.ts +113 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
- package/src/extensibility/gjc-plugins/schema.ts +121 -4
- package/src/extensibility/gjc-plugins/types.ts +25 -0
- package/src/extensibility/hooks/loader.ts +133 -16
- package/src/extensibility/hooks/types.ts +28 -0
- package/src/extensibility/runtime-skill-discovery.ts +149 -190
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skill-management.ts +200 -52
- package/src/extensibility/skills.ts +73 -25
- package/src/gjc-runtime/empty-delete-gc.ts +94 -4
- package/src/gjc-runtime/gc-runtime.ts +29 -0
- package/src/gjc-runtime/linux-proc.ts +14 -0
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +143 -10
- package/src/gjc-runtime/session-state-sidecar.ts +27 -11
- package/src/gjc-runtime/state-writer.ts +30 -29
- package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
- package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -0
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +12 -12
- package/src/lsp/config.ts +10 -3
- package/src/lsp/index.ts +1 -1
- package/src/lsp/path-trust.ts +69 -14
- package/src/main.ts +78 -35
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/acp/acp-agent.ts +16 -3
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/bash-execution.ts +4 -1
- package/src/modes/components/execution-shared.ts +4 -1
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/session-observer-overlay.ts +1 -1
- package/src/modes/components/settings-selector.ts +1075 -235
- package/src/modes/components/status-line/command.ts +190 -0
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/segments.ts +32 -5
- package/src/modes/components/status-line/types.ts +5 -0
- package/src/modes/components/tool-status-header.ts +396 -22
- package/src/modes/controllers/command-controller.ts +3 -0
- package/src/modes/controllers/event-controller.ts +151 -7
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/input-controller.ts +6 -3
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +53 -6
- package/src/modes/print-mode.ts +5 -1
- package/src/modes/prompt-action-autocomplete.ts +54 -27
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/types.ts +17 -3
- package/src/modes/utils/ui-helpers.ts +2 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/search.md +2 -0
- package/src/prompts/tools/subagent.md +1 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +189 -14
- package/src/runtime-mcp/pool.test.ts +218 -0
- package/src/runtime-mcp/pool.ts +224 -23
- package/src/runtime-mcp/transports/stdio.ts +158 -24
- package/src/runtime-mcp/types.ts +24 -0
- package/src/sdk/broker/broker.ts +124 -32
- package/src/sdk/broker/ensure.ts +248 -41
- package/src/sdk/broker/lifecycle-ledger.ts +7 -17
- package/src/sdk/broker/lifecycle.ts +56 -6
- package/src/sdk/broker/startup-failure.ts +15 -6
- package/src/sdk/bus/chat-daemon-control.ts +3 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
- package/src/sdk/bus/chat-effect-journal.ts +132 -7
- package/src/sdk/bus/conversation-store.ts +29 -0
- package/src/sdk/bus/lifecycle-commands.ts +17 -11
- package/src/sdk/bus/slack-conversation.ts +1 -0
- package/src/sdk/bus/slack-daemon.ts +478 -49
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/bus/telegram-daemon.ts +16 -2
- package/src/sdk/bus/topic-registry.ts +19 -0
- package/src/sdk/cli/master-cli.ts +1 -1
- package/src/sdk/cli/session-cli.ts +11 -8
- package/src/sdk/client/client.ts +66 -17
- package/src/sdk/host/host.ts +29 -9
- package/src/sdk/host/query/revision-store.ts +19 -13
- package/src/sdk/host/reverse-leases.ts +7 -4
- package/src/sdk/host/session-runtime.test.ts +52 -10
- package/src/sdk/host/session-runtime.ts +383 -44
- package/src/sdk/lifecycle/service.ts +4 -0
- package/src/sdk/protocol/operation-inventory.generated.json +77 -0
- package/src/sdk/router/session-router.ts +89 -38
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +228 -55
- package/src/sdk/transport/relay.ts +35 -13
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +240 -90
- package/src/session/contribution-prep.ts +214 -21
- package/src/session/fallback-chain-controller.ts +38 -0
- package/src/session/internal/managed-session-storage.ts +19 -5
- package/src/session/messages.ts +5 -0
- package/src/session/session-manager.ts +29 -5
- package/src/session-import/redact.ts +5 -1
- package/src/setup/paseo/announce.ts +456 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/skill-state/canonical-skills.ts +12 -0
- package/src/skills/skills.test.ts +41 -21
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +29 -3
- package/src/task/executor.ts +7 -1
- package/src/task/index.ts +21 -4
- package/src/tools/bash.ts +63 -19
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/gh.ts +26 -3
- package/src/tools/github-cache.ts +68 -16
- package/src/tools/index.ts +10 -0
- package/src/tools/job.ts +21 -4
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/read.ts +14 -2
- package/src/tools/render-utils.ts +1 -3
- package/src/tools/resource-gc.ts +28 -8
- package/src/tools/search.ts +116 -58
- package/src/tools/skill-discovery.ts +5 -0
- package/src/tools/skill.ts +37 -2
- package/src/tools/subagent.ts +6 -3
- package/src/tools/tool-catalog.generated.ts +10 -3
- package/src/tools/ultragoal-ask-guard.ts +2 -2
- package/src/web/insane/bridge.ts +113 -23
- package/src/web/search/providers/perplexity.ts +102 -41
package/src/discovery/helpers.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import type { FileHandle } from "node:fs/promises";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { ThinkingLevel } from "@gajae-code/agent-core";
|
|
4
5
|
import type { FileType as FileTypeEnum, glob as globFn } from "@gajae-code/natives";
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
getProjectDir,
|
|
10
11
|
getTrustedHomeDir,
|
|
11
12
|
logger,
|
|
13
|
+
normalizePathForComparison,
|
|
12
14
|
parseFrontmatter,
|
|
13
15
|
tryParseJson,
|
|
14
16
|
} from "@gajae-code/utils";
|
|
@@ -99,7 +101,32 @@ export const SOURCE_PATHS = {
|
|
|
99
101
|
},
|
|
100
102
|
} as const;
|
|
101
103
|
|
|
104
|
+
/** Ancestors from `cwd` through `stopAt`, excluding one normalized authority boundary. */
|
|
105
|
+
export function getAncestorDirs(
|
|
106
|
+
cwd: string,
|
|
107
|
+
stopAt?: string | null,
|
|
108
|
+
excludeDir?: string,
|
|
109
|
+
): Array<{ dir: string; depth: number }> {
|
|
110
|
+
const ancestors: Array<{ dir: string; depth: number }> = [];
|
|
111
|
+
let current = path.resolve(cwd);
|
|
112
|
+
let depth = 0;
|
|
113
|
+
let normalizedCurrent = normalizePathForComparison(current);
|
|
114
|
+
const normalizedStop = stopAt ? normalizePathForComparison(path.resolve(stopAt)) : undefined;
|
|
115
|
+
const normalizedExclude = excludeDir ? normalizePathForComparison(path.resolve(excludeDir)) : undefined;
|
|
116
|
+
while (true) {
|
|
117
|
+
if (normalizedCurrent !== normalizedExclude) ancestors.push({ dir: current, depth });
|
|
118
|
+
if (normalizedStop && normalizedCurrent === normalizedStop) break;
|
|
119
|
+
const parent = path.dirname(current);
|
|
120
|
+
if (parent === current) break;
|
|
121
|
+
current = parent;
|
|
122
|
+
normalizedCurrent = normalizePathForComparison(current);
|
|
123
|
+
depth++;
|
|
124
|
+
}
|
|
125
|
+
return ancestors;
|
|
126
|
+
}
|
|
127
|
+
|
|
102
128
|
export type SourceId = keyof typeof SOURCE_PATHS;
|
|
129
|
+
export type ProfileAuthority = "default" | "custom";
|
|
103
130
|
|
|
104
131
|
/**
|
|
105
132
|
* Get user-level path for a source.
|
|
@@ -110,6 +137,49 @@ export function getUserPath(ctx: LoadContext, source: SourceId, subpath: string)
|
|
|
110
137
|
return path.join(ctx.home, paths.userAgent, subpath);
|
|
111
138
|
}
|
|
112
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Deterministic user-scope skill scan directories, highest precedence first.
|
|
142
|
+
*
|
|
143
|
+
* The canonical root is the agent directory (`getAgentDir()`, moved by
|
|
144
|
+
* `--agent-dir` / `GJC_CODING_AGENT_DIR` / `setAgentDir()`) — the target of
|
|
145
|
+
* every user-scope skill writer (`gjc migrate`, `gjc skill`). An agent-directory
|
|
146
|
+
* profile is a *separate* user scope, the same contract as MCP user config
|
|
147
|
+
* (#4768): its legacy home-relative roots are not scanned, so a profile cannot
|
|
148
|
+
* pick up the default profile's skills (and vice versa). The resolver-owned
|
|
149
|
+
* `profileAuthority` classification is authoritative when supplied, so a
|
|
150
|
+
* custom profile remains isolated even if a later HOME/config-root refresh
|
|
151
|
+
* makes its path look like the current default. In the default profile the
|
|
152
|
+
* agent directory is `<home>/<configDir>/agent` and the configured legacy roots
|
|
153
|
+
* below it are still honored, exactly as before.
|
|
154
|
+
*/
|
|
155
|
+
export function resolveUserAgentDir(home: string, userAgentDir?: string): string {
|
|
156
|
+
return path.resolve(userAgentDir ?? path.join(home, SOURCE_PATHS.native.userAgent));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getUserSkillScanDirs(
|
|
160
|
+
home: string,
|
|
161
|
+
userAgentDir?: string,
|
|
162
|
+
profileAuthority?: ProfileAuthority,
|
|
163
|
+
): string[] {
|
|
164
|
+
const resolvedAgentDir = resolveUserAgentDir(home, userAgentDir);
|
|
165
|
+
const defaultAgentDir = path.join(home, SOURCE_PATHS.native.userAgent);
|
|
166
|
+
const authority =
|
|
167
|
+
profileAuthority ??
|
|
168
|
+
(normalizePathForComparison(resolvedAgentDir) === normalizePathForComparison(defaultAgentDir)
|
|
169
|
+
? "default"
|
|
170
|
+
: "custom");
|
|
171
|
+
if (authority === "custom") {
|
|
172
|
+
return [path.join(resolvedAgentDir, "skills")];
|
|
173
|
+
}
|
|
174
|
+
return [
|
|
175
|
+
...new Set([
|
|
176
|
+
path.join(resolvedAgentDir, "skills"),
|
|
177
|
+
path.join(home, SOURCE_PATHS.native.userBase, "skills"),
|
|
178
|
+
path.join(home, ".gjc", "skills"),
|
|
179
|
+
]),
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
|
|
113
183
|
/**
|
|
114
184
|
* Get project-level path for a source (cwd only).
|
|
115
185
|
*/
|
|
@@ -345,6 +415,8 @@ async function globIf(
|
|
|
345
415
|
|
|
346
416
|
export interface ScanSkillsFromDirOptions {
|
|
347
417
|
dir: string;
|
|
418
|
+
/** Selected authority root whose own identity must not be a symlink. */
|
|
419
|
+
authorityRoot?: string;
|
|
348
420
|
providerId: string;
|
|
349
421
|
level: "user" | "project";
|
|
350
422
|
requireDescription?: boolean;
|
|
@@ -365,30 +437,40 @@ export const SKILL_FRONTMATTER_SCAN_BYTES = 4 * 1024;
|
|
|
365
437
|
/** Maximum total bytes read while seeking the frontmatter closing delimiter. */
|
|
366
438
|
export const SKILL_FRONTMATTER_SCAN_TOTAL_BYTES = 64 * 1024;
|
|
367
439
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
440
|
+
export const SkillDiscoveryTestHooks: {
|
|
441
|
+
afterCandidateValidated?: (candidatePath: string) => void | Promise<void>;
|
|
442
|
+
afterAuthorityRootValidated?: (root: string) => void | Promise<void>;
|
|
443
|
+
afterAuthorityRootMissing?: (root: string) => void | Promise<void>;
|
|
444
|
+
afterContainedRootValidated?: (root: string) => void | Promise<void>;
|
|
445
|
+
} = {};
|
|
446
|
+
|
|
447
|
+
interface DirectoryIdentity {
|
|
448
|
+
dev: bigint;
|
|
449
|
+
ino: bigint;
|
|
450
|
+
realPath: string;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function captureDirectoryIdentity(root: string): Promise<DirectoryIdentity | null> {
|
|
454
|
+
const observed = await fs.promises.lstat(root, { bigint: true });
|
|
455
|
+
if (observed.isSymbolicLink() || !observed.isDirectory()) return null;
|
|
456
|
+
const realPath = await fs.promises.realpath(root);
|
|
457
|
+
const resolved = await fs.promises.stat(realPath, { bigint: true });
|
|
458
|
+
if (!resolved.isDirectory() || observed.dev !== resolved.dev || observed.ino !== resolved.ino) return null;
|
|
459
|
+
return { dev: observed.dev, ino: observed.ino, realPath };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function directoryIdentityIsCurrent(root: string, expected: DirectoryIdentity): Promise<boolean> {
|
|
463
|
+
try {
|
|
464
|
+
const current = await captureDirectoryIdentity(root);
|
|
465
|
+
return (
|
|
466
|
+
current !== null &&
|
|
467
|
+
current.dev === expected.dev &&
|
|
468
|
+
current.ino === expected.ino &&
|
|
469
|
+
normalizePathForComparison(current.realPath) === normalizePathForComparison(expected.realPath)
|
|
470
|
+
);
|
|
471
|
+
} catch {
|
|
472
|
+
return false;
|
|
390
473
|
}
|
|
391
|
-
return null;
|
|
392
474
|
}
|
|
393
475
|
|
|
394
476
|
export async function scanSkillsFromDir(
|
|
@@ -400,19 +482,150 @@ export async function scanSkillsFromDir(
|
|
|
400
482
|
const { dir, level, providerId, requireDescription = false } = options;
|
|
401
483
|
|
|
402
484
|
let entries: fs.Dirent[];
|
|
485
|
+
let realRoot: string;
|
|
486
|
+
let authorityIdentity: DirectoryIdentity | null = null;
|
|
487
|
+
let scanRootIdentity: DirectoryIdentity;
|
|
403
488
|
try {
|
|
489
|
+
if (options.authorityRoot) {
|
|
490
|
+
try {
|
|
491
|
+
authorityIdentity = await captureDirectoryIdentity(options.authorityRoot);
|
|
492
|
+
if (!authorityIdentity) {
|
|
493
|
+
warnings.push(`Refusing unsafe skill authority root: ${options.authorityRoot}`);
|
|
494
|
+
return { items, warnings };
|
|
495
|
+
}
|
|
496
|
+
await SkillDiscoveryTestHooks.afterAuthorityRootValidated?.(options.authorityRoot);
|
|
497
|
+
if (!(await directoryIdentityIsCurrent(options.authorityRoot, authorityIdentity))) {
|
|
498
|
+
warnings.push(`Refusing changed skill authority root: ${options.authorityRoot}`);
|
|
499
|
+
return { items, warnings };
|
|
500
|
+
}
|
|
501
|
+
} catch (error) {
|
|
502
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
503
|
+
await SkillDiscoveryTestHooks.afterAuthorityRootMissing?.(options.authorityRoot);
|
|
504
|
+
return { items, warnings };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const capturedScanRoot = await captureDirectoryIdentity(dir);
|
|
508
|
+
if (!capturedScanRoot) {
|
|
509
|
+
warnings.push(`Refusing unsafe skills directory: ${dir}`);
|
|
510
|
+
return { items, warnings };
|
|
511
|
+
}
|
|
512
|
+
scanRootIdentity = capturedScanRoot;
|
|
513
|
+
realRoot = scanRootIdentity.realPath;
|
|
404
514
|
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
515
|
+
if (!(await directoryIdentityIsCurrent(dir, scanRootIdentity))) {
|
|
516
|
+
warnings.push(`Refusing changed skills directory: ${dir}`);
|
|
517
|
+
return { items, warnings };
|
|
518
|
+
}
|
|
405
519
|
} catch (error) {
|
|
406
520
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
407
521
|
warnings.push(`Failed to read skills directory: ${dir} (${String(error)})`);
|
|
408
522
|
}
|
|
409
523
|
return { items, warnings };
|
|
410
524
|
}
|
|
411
|
-
const
|
|
525
|
+
const isWithinRoot = (candidate: string): boolean => {
|
|
526
|
+
const relative = path.relative(realRoot, candidate);
|
|
527
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
528
|
+
};
|
|
529
|
+
const openSkillFileSafely = async (skillPath: string): Promise<FileHandle> => {
|
|
530
|
+
if (
|
|
531
|
+
!(await directoryIdentityIsCurrent(dir, scanRootIdentity)) ||
|
|
532
|
+
(options.authorityRoot &&
|
|
533
|
+
authorityIdentity &&
|
|
534
|
+
!(await directoryIdentityIsCurrent(options.authorityRoot, authorityIdentity)))
|
|
535
|
+
) {
|
|
536
|
+
throw new Error(`Unsafe skill authority root for: ${skillPath}`);
|
|
537
|
+
}
|
|
538
|
+
const flags =
|
|
539
|
+
fs.constants.O_RDONLY |
|
|
540
|
+
(process.platform === "win32" ? 0 : (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
541
|
+
const handle = await fs.promises.open(skillPath, flags);
|
|
412
542
|
try {
|
|
413
|
-
const
|
|
543
|
+
const [opened, observed, currentPath, currentScanRoot, currentAuthorityRoot] = await Promise.all([
|
|
544
|
+
handle.stat({ bigint: true }),
|
|
545
|
+
fs.promises.lstat(skillPath, { bigint: true }),
|
|
546
|
+
fs.promises.realpath(skillPath),
|
|
547
|
+
directoryIdentityIsCurrent(dir, scanRootIdentity),
|
|
548
|
+
options.authorityRoot && authorityIdentity
|
|
549
|
+
? directoryIdentityIsCurrent(options.authorityRoot, authorityIdentity)
|
|
550
|
+
: true,
|
|
551
|
+
]);
|
|
552
|
+
if (
|
|
553
|
+
!opened.isFile() ||
|
|
554
|
+
opened.nlink !== 1n ||
|
|
555
|
+
observed.isSymbolicLink() ||
|
|
556
|
+
!observed.isFile() ||
|
|
557
|
+
opened.dev !== observed.dev ||
|
|
558
|
+
opened.ino !== observed.ino ||
|
|
559
|
+
!isWithinRoot(currentPath) ||
|
|
560
|
+
!currentScanRoot ||
|
|
561
|
+
!currentAuthorityRoot
|
|
562
|
+
) {
|
|
563
|
+
throw new Error(`Unsafe skill file: ${skillPath}`);
|
|
564
|
+
}
|
|
565
|
+
return handle;
|
|
566
|
+
} catch (error) {
|
|
567
|
+
await handle.close();
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
const readSkillContentSafely = async (skillPath: string): Promise<string> => {
|
|
572
|
+
const handle = await openSkillFileSafely(skillPath);
|
|
573
|
+
try {
|
|
574
|
+
return await handle.readFile({ encoding: "utf8" });
|
|
575
|
+
} finally {
|
|
576
|
+
await handle.close();
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
const readSkillFrontmatterSafely = async (
|
|
580
|
+
skillPath: string,
|
|
581
|
+
): Promise<{ frontmatter: SkillFrontmatter | null; size: number }> => {
|
|
582
|
+
const handle = await openSkillFileSafely(skillPath);
|
|
583
|
+
try {
|
|
584
|
+
const size = Number((await handle.stat({ bigint: true })).size);
|
|
585
|
+
const scanLimit = Math.min(size, SKILL_FRONTMATTER_SCAN_TOTAL_BYTES);
|
|
586
|
+
let offset = 0;
|
|
587
|
+
let prefix = "";
|
|
588
|
+
const decoder = new TextDecoder();
|
|
589
|
+
while (offset < scanLimit) {
|
|
590
|
+
const length = Math.min(SKILL_FRONTMATTER_SCAN_BYTES, scanLimit - offset);
|
|
591
|
+
const bytes = new Uint8Array(length);
|
|
592
|
+
const { bytesRead } = await handle.read(bytes, 0, length, offset);
|
|
593
|
+
if (bytesRead === 0) break;
|
|
594
|
+
offset += bytesRead;
|
|
595
|
+
prefix += decoder.decode(bytes.subarray(0, bytesRead), { stream: offset < scanLimit });
|
|
596
|
+
|
|
597
|
+
const opening = prefix.match(/^---[ \t]*(?:\r?\n|$)/);
|
|
598
|
+
if (!opening) return { frontmatter: null, size };
|
|
599
|
+
const afterOpening = prefix.slice(opening[0].length);
|
|
600
|
+
const closing = afterOpening.match(/\r?\n---[ \t]*(?:\r?\n|$)/);
|
|
601
|
+
if (!closing || closing.index === undefined) continue;
|
|
602
|
+
const bounded = prefix.slice(0, opening[0].length + closing.index + closing[0].length);
|
|
603
|
+
return {
|
|
604
|
+
frontmatter: parseFrontmatter(bounded, { source: skillPath }).frontmatter as SkillFrontmatter,
|
|
605
|
+
size,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
return { frontmatter: null, size };
|
|
609
|
+
} finally {
|
|
610
|
+
await handle.close();
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
const loadSkill = async (candidatePath: string) => {
|
|
614
|
+
try {
|
|
615
|
+
const skillPath = await fs.promises.realpath(candidatePath);
|
|
616
|
+
if (!isWithinRoot(skillPath)) {
|
|
617
|
+
warnings.push(`Refusing skill path outside scan root: ${candidatePath}`);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const stat = await fs.promises.stat(skillPath);
|
|
621
|
+
if (!stat.isFile() || stat.nlink !== 1) {
|
|
622
|
+
warnings.push(`Skill path is not a regular file: ${candidatePath}`);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
await SkillDiscoveryTestHooks.afterCandidateValidated?.(candidatePath);
|
|
626
|
+
const { frontmatter, size } = await readSkillFrontmatterSafely(skillPath);
|
|
414
627
|
if (!frontmatter) {
|
|
415
|
-
if (
|
|
628
|
+
if (size > SKILL_FRONTMATTER_SCAN_TOTAL_BYTES) {
|
|
416
629
|
warnings.push(
|
|
417
630
|
`Skill frontmatter exceeded ${SKILL_FRONTMATTER_SCAN_TOTAL_BYTES} byte scan cap: ${skillPath}`,
|
|
418
631
|
);
|
|
@@ -435,15 +648,16 @@ export async function scanSkillsFromDir(
|
|
|
435
648
|
name,
|
|
436
649
|
path: skillPath,
|
|
437
650
|
loadContent: async () => {
|
|
438
|
-
const content = await
|
|
651
|
+
const content = await readSkillContentSafely(skillPath);
|
|
439
652
|
return parseFrontmatter(content, { source: skillPath }).body;
|
|
440
653
|
},
|
|
441
654
|
frontmatter: frontmatter as SkillFrontmatter,
|
|
442
655
|
level,
|
|
443
656
|
_source: createSourceMeta(providerId, skillPath, level),
|
|
444
657
|
});
|
|
445
|
-
} catch {
|
|
446
|
-
|
|
658
|
+
} catch (error) {
|
|
659
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
660
|
+
warnings.push(`Failed to read skill file: ${candidatePath}`);
|
|
447
661
|
}
|
|
448
662
|
};
|
|
449
663
|
|
|
@@ -452,9 +666,7 @@ export async function scanSkillsFromDir(
|
|
|
452
666
|
if (entry.name.startsWith(".")) continue;
|
|
453
667
|
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
454
668
|
const skillPath = path.join(dir, entry.name, "SKILL.md");
|
|
455
|
-
|
|
456
|
-
work.push(loadSkill(skillPath));
|
|
457
|
-
}
|
|
669
|
+
work.push(loadSkill(skillPath));
|
|
458
670
|
}
|
|
459
671
|
await Promise.all(work);
|
|
460
672
|
|
|
@@ -464,6 +676,59 @@ export async function scanSkillsFromDir(
|
|
|
464
676
|
return { items, warnings };
|
|
465
677
|
}
|
|
466
678
|
|
|
679
|
+
/** Read a regular single-link file that remains contained by its configured root. */
|
|
680
|
+
export async function readContainedFile(root: string, filePath: string): Promise<string | null> {
|
|
681
|
+
let rootIdentity: DirectoryIdentity;
|
|
682
|
+
try {
|
|
683
|
+
const capturedRoot = await captureDirectoryIdentity(root);
|
|
684
|
+
if (!capturedRoot) return null;
|
|
685
|
+
rootIdentity = capturedRoot;
|
|
686
|
+
await SkillDiscoveryTestHooks.afterContainedRootValidated?.(root);
|
|
687
|
+
if (!(await directoryIdentityIsCurrent(root, rootIdentity))) return null;
|
|
688
|
+
} catch (error) {
|
|
689
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
690
|
+
throw error;
|
|
691
|
+
}
|
|
692
|
+
const relative = path.relative(root, filePath);
|
|
693
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) return null;
|
|
694
|
+
const flags =
|
|
695
|
+
fs.constants.O_RDONLY |
|
|
696
|
+
(process.platform === "win32" ? 0 : (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
697
|
+
let handle: FileHandle;
|
|
698
|
+
try {
|
|
699
|
+
handle = await fs.promises.open(filePath, flags);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
const [opened, observed, currentPath, currentRoot] = await Promise.all([
|
|
706
|
+
handle.stat({ bigint: true }),
|
|
707
|
+
fs.promises.lstat(filePath, { bigint: true }),
|
|
708
|
+
fs.promises.realpath(filePath),
|
|
709
|
+
directoryIdentityIsCurrent(root, rootIdentity),
|
|
710
|
+
]);
|
|
711
|
+
const currentRelative = path.relative(rootIdentity.realPath, currentPath);
|
|
712
|
+
if (
|
|
713
|
+
!opened.isFile() ||
|
|
714
|
+
opened.nlink !== 1n ||
|
|
715
|
+
observed.isSymbolicLink() ||
|
|
716
|
+
!observed.isFile() ||
|
|
717
|
+
opened.dev !== observed.dev ||
|
|
718
|
+
opened.ino !== observed.ino ||
|
|
719
|
+
!currentRoot ||
|
|
720
|
+
currentRelative.startsWith("..") ||
|
|
721
|
+
path.isAbsolute(currentRelative)
|
|
722
|
+
) {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
const content = await handle.readFile({ encoding: "utf8" });
|
|
726
|
+
return (await directoryIdentityIsCurrent(root, rootIdentity)) ? content : null;
|
|
727
|
+
} finally {
|
|
728
|
+
await handle.close();
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
467
732
|
/**
|
|
468
733
|
* Expand environment variables in a string.
|
|
469
734
|
* Supports ${VAR} and ${VAR:-default} syntax.
|
|
@@ -852,6 +1117,14 @@ export async function resolveOrDefaultProjectRegistryPath(cwd: string): Promise<
|
|
|
852
1117
|
|
|
853
1118
|
const pluginRootsCache = new Map<string, { roots: ClaudePluginRoot[]; warnings: string[] }>();
|
|
854
1119
|
|
|
1120
|
+
/** Invalidate the exact user/project registry pair and their resolved-root cache entry. */
|
|
1121
|
+
export async function invalidateClaudePluginRoots(home: string, cwd?: string): Promise<void> {
|
|
1122
|
+
const resolvedProjectPath = cwd ? await resolveActiveProjectRegistryPath(cwd) : null;
|
|
1123
|
+
invalidateFsCache(path.join(getPluginsDir(home), "installed_plugins.json"));
|
|
1124
|
+
if (resolvedProjectPath) invalidateFsCache(resolvedProjectPath);
|
|
1125
|
+
pluginRootsCache.delete(`${home}:${resolvedProjectPath ?? ""}`);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
855
1128
|
/**
|
|
856
1129
|
* List installed GJC plugin roots from the GJC plugin registry and, when present,
|
|
857
1130
|
* the nearest project-scoped registry resolved from `cwd`.
|
|
@@ -885,7 +1158,7 @@ export async function listClaudePluginRoots(
|
|
|
885
1158
|
if (!Array.isArray(entries) || entries.length === 0) continue;
|
|
886
1159
|
|
|
887
1160
|
const atIndex = pluginId.lastIndexOf("@");
|
|
888
|
-
if (atIndex === -1) {
|
|
1161
|
+
if (atIndex <= 0 || atIndex === pluginId.length - 1) {
|
|
889
1162
|
warnings.push(`Invalid plugin ID format (missing @marketplace): ${pluginId}`);
|
|
890
1163
|
continue;
|
|
891
1164
|
}
|
|
@@ -927,7 +1200,7 @@ export async function listClaudePluginRoots(
|
|
|
927
1200
|
for (const [pluginId, entries] of Object.entries(projectRegistry.plugins)) {
|
|
928
1201
|
if (!Array.isArray(entries) || entries.length === 0) continue;
|
|
929
1202
|
const atIndex = pluginId.lastIndexOf("@");
|
|
930
|
-
if (atIndex === -1) {
|
|
1203
|
+
if (atIndex <= 0 || atIndex === pluginId.length - 1) {
|
|
931
1204
|
warnings.push(`Invalid plugin ID format (missing @marketplace): ${pluginId}`);
|
|
932
1205
|
continue;
|
|
933
1206
|
}
|