@cjhyy/code-shell-core 0.5.0-rc.1 → 0.5.0-rc.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/dist/agent/agent-definition-registry.d.ts +18 -3
- package/dist/agent/agent-definition-registry.js +47 -18
- package/dist/agent/agent-definition.d.ts +17 -0
- package/dist/agent/agent-definition.js +22 -1
- package/dist/arena/arena.js +11 -12
- package/dist/arena/context/context-tools.js +2 -4
- package/dist/arena/context/within-root.d.ts +7 -0
- package/dist/arena/context/within-root.js +15 -0
- package/dist/arena/iterate/phases/argue.js +8 -2
- package/dist/arena/iterate/phases/revise.js +1 -1
- package/dist/arena/iterate/phases/tournament.js +3 -3
- package/dist/arena/phases/adjudication.js +1 -4
- package/dist/arena/phases/build-consensus.js +1 -4
- package/dist/arena/phases/cross-review.js +2 -8
- package/dist/arena/phases/debate-rounds.js +1 -4
- package/dist/arena/phases/participant-research.js +1 -4
- package/dist/arena/phases/planning-detail-expansion.js +8 -10
- package/dist/arena/planner.js +0 -1
- package/dist/arena/providers/docs.d.ts +1 -0
- package/dist/arena/providers/docs.js +5 -2
- package/dist/arena/providers/repo.d.ts +1 -0
- package/dist/arena/providers/repo.js +5 -2
- package/dist/arena/strategies/utils.js +36 -3
- package/dist/arena/transitions.d.ts +7 -2
- package/dist/arena/transitions.js +9 -4
- package/dist/arena/types.d.ts +8 -2
- package/dist/automation/cron-expr.d.ts +31 -0
- package/dist/automation/cron-expr.js +151 -0
- package/dist/automation/index.d.ts +41 -0
- package/dist/automation/index.js +39 -0
- package/dist/automation/runner.d.ts +67 -0
- package/dist/automation/runner.js +56 -0
- package/dist/automation/scheduler.d.ts +156 -0
- package/dist/automation/scheduler.js +546 -0
- package/dist/automation/store.d.ts +39 -0
- package/dist/automation/store.js +119 -0
- package/dist/automation/write-policy.d.ts +34 -0
- package/dist/automation/write-policy.js +113 -0
- package/dist/automation/write-run.d.ts +51 -0
- package/dist/automation/write-run.js +38 -0
- package/dist/capability-control/index.d.ts +5 -0
- package/dist/capability-control/index.js +3 -0
- package/dist/capability-control/overlay.d.ts +49 -0
- package/dist/capability-control/overlay.js +89 -0
- package/dist/capability-control/project.d.ts +65 -0
- package/dist/capability-control/project.js +147 -0
- package/dist/capability-control/service.d.ts +70 -0
- package/dist/capability-control/service.js +147 -0
- package/dist/capability-control/types.d.ts +62 -0
- package/dist/capability-control/types.js +17 -0
- package/dist/cli/agent-server-stdio.d.ts +12 -1
- package/dist/cli/agent-server-stdio.js +114 -19
- package/dist/cli/agent-server-tcp.d.ts +20 -0
- package/dist/cli/agent-server-tcp.js +120 -0
- package/dist/cli/graceful-shutdown.d.ts +27 -0
- package/dist/cli/graceful-shutdown.js +30 -0
- package/dist/context/compaction.js +23 -15
- package/dist/context/manager.d.ts +0 -20
- package/dist/context/manager.js +0 -37
- package/dist/cron/cron-runtime.d.ts +2 -0
- package/dist/cron/cron-runtime.js +2 -0
- package/dist/cron/cron-store.d.ts +2 -0
- package/dist/cron/cron-store.js +2 -0
- package/dist/cron/scheduler.d.ts +5 -32
- package/dist/cron/scheduler.js +5 -113
- package/dist/data/openrouter-sync.js +1 -1
- package/dist/engine/engine.d.ts +208 -14
- package/dist/engine/engine.js +754 -169
- package/dist/engine/goal.d.ts +42 -0
- package/dist/engine/goal.js +52 -0
- package/dist/engine/image-compression.d.ts +81 -0
- package/dist/engine/image-compression.js +186 -0
- package/dist/engine/image-policy.d.ts +152 -0
- package/dist/engine/image-policy.js +165 -0
- package/dist/engine/reactive-threshold.d.ts +13 -0
- package/dist/engine/reactive-threshold.js +17 -0
- package/dist/engine/runtime.js +7 -0
- package/dist/engine/session-title.d.ts +13 -0
- package/dist/engine/session-title.js +34 -0
- package/dist/engine/turn-loop.d.ts +29 -8
- package/dist/engine/turn-loop.js +498 -316
- package/dist/git/parse-log.d.ts +13 -0
- package/dist/git/parse-log.js +21 -0
- package/dist/git/utils.d.ts +8 -6
- package/dist/git/utils.js +57 -26
- package/dist/git/worktree.js +33 -18
- package/dist/hooks/events.d.ts +23 -1
- package/dist/hooks/goal-stop-hook.d.ts +30 -0
- package/dist/hooks/goal-stop-hook.js +102 -0
- package/dist/hooks/hook-output.d.ts +33 -0
- package/dist/hooks/hook-output.js +79 -0
- package/dist/hooks/registry.d.ts +7 -0
- package/dist/hooks/registry.js +38 -1
- package/dist/hooks/shell-runner.d.ts +2 -0
- package/dist/hooks/shell-runner.js +43 -2
- package/dist/index.d.ts +31 -6
- package/dist/index.js +30 -3
- package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
- package/dist/llm/capabilities/reasoning-control.js +38 -0
- package/dist/llm/capabilities/rules.js +33 -3
- package/dist/llm/capabilities/types.d.ts +22 -2
- package/dist/llm/clamp-max-tokens.d.ts +11 -0
- package/dist/llm/clamp-max-tokens.js +17 -0
- package/dist/llm/client-base.d.ts +41 -4
- package/dist/llm/client-base.js +67 -13
- package/dist/llm/client-factory.d.ts +3 -3
- package/dist/llm/client-factory.js +2 -2
- package/dist/llm/model-pool.d.ts +15 -11
- package/dist/llm/model-pool.js +23 -20
- package/dist/llm/provider-catalog.d.ts +2 -2
- package/dist/llm/providers/anthropic.d.ts +32 -2
- package/dist/llm/providers/anthropic.js +147 -8
- package/dist/llm/providers/openai.d.ts +11 -2
- package/dist/llm/providers/openai.js +253 -52
- package/dist/llm/reasoning-setting.d.ts +51 -0
- package/dist/llm/reasoning-setting.js +32 -0
- package/dist/llm/stop-reason.d.ts +13 -0
- package/dist/llm/stop-reason.js +21 -0
- package/dist/llm/strip-vision.d.ts +32 -0
- package/dist/llm/strip-vision.js +94 -0
- package/dist/llm/types.d.ts +5 -6
- package/dist/logging/logger.js +18 -4
- package/dist/logging/sanitize-messages.d.ts +10 -0
- package/dist/logging/sanitize-messages.js +90 -0
- package/dist/lsp/client.js +28 -8
- package/dist/lsp/manager.js +2 -1
- package/dist/lsp/root-path.d.ts +9 -0
- package/dist/lsp/root-path.js +12 -0
- package/dist/onboarding.js +18 -11
- package/dist/plugins/gitOps.d.ts +34 -0
- package/dist/plugins/gitOps.js +59 -3
- package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
- package/dist/plugins/installer/codex/convertAgents.js +55 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
- package/dist/plugins/installer/codex/convertMcp.js +45 -0
- package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
- package/dist/plugins/installer/codex/convertSkills.js +33 -0
- package/dist/plugins/installer/detectFormat.d.ts +2 -0
- package/dist/plugins/installer/detectFormat.js +6 -0
- package/dist/plugins/installer/install.d.ts +7 -0
- package/dist/plugins/installer/install.js +99 -0
- package/dist/plugins/installer/installFromSource.d.ts +13 -0
- package/dist/plugins/installer/installFromSource.js +45 -0
- package/dist/plugins/installer/list.d.ts +9 -0
- package/dist/plugins/installer/list.js +30 -0
- package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
- package/dist/plugins/installer/loadPluginAgents.js +23 -0
- package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
- package/dist/plugins/installer/loadPluginMcp.js +74 -0
- package/dist/plugins/installer/parseSource.d.ts +20 -0
- package/dist/plugins/installer/parseSource.js +74 -0
- package/dist/plugins/installer/paths.d.ts +5 -0
- package/dist/plugins/installer/paths.js +25 -0
- package/dist/plugins/installer/types.d.ts +49 -0
- package/dist/plugins/installer/types.js +27 -0
- package/dist/plugins/installer/uninstall.d.ts +2 -0
- package/dist/plugins/installer/uninstall.js +14 -0
- package/dist/plugins/installer/update.d.ts +14 -0
- package/dist/plugins/installer/update.js +65 -0
- package/dist/plugins/loadPluginHooks.d.ts +8 -1
- package/dist/plugins/loadPluginHooks.js +11 -1
- package/dist/plugins/marketplaceManager.d.ts +8 -1
- package/dist/plugins/marketplaceManager.js +38 -4
- package/dist/plugins/pluginCommandHook.js +34 -0
- package/dist/plugins/pluginInstaller.d.ts +21 -0
- package/dist/plugins/pluginInstaller.js +128 -29
- package/dist/plugins/schemas.js +40 -6
- package/dist/plugins/types.d.ts +4 -0
- package/dist/preset/index.js +16 -0
- package/dist/prompt/composer.d.ts +4 -0
- package/dist/prompt/composer.js +28 -4
- package/dist/prompt/sections/orchestration.md +8 -0
- package/dist/protocol/chat-session-manager.d.ts +8 -1
- package/dist/protocol/chat-session-manager.js +10 -0
- package/dist/protocol/chat-session.d.ts +17 -0
- package/dist/protocol/chat-session.js +28 -0
- package/dist/protocol/redact.d.ts +50 -0
- package/dist/protocol/redact.js +71 -0
- package/dist/protocol/server.d.ts +41 -2
- package/dist/protocol/server.js +231 -33
- package/dist/protocol/tcp-transport.d.ts +45 -0
- package/dist/protocol/tcp-transport.js +74 -0
- package/dist/protocol/types.d.ts +23 -0
- package/dist/protocol/types.js +6 -0
- package/dist/remote/bridge.d.ts +11 -2
- package/dist/remote/bridge.js +61 -33
- package/dist/run/ArtifactTracker.js +5 -4
- package/dist/run/EngineRunner.d.ts +27 -1
- package/dist/run/EngineRunner.js +46 -8
- package/dist/run/FileRunStore.js +32 -8
- package/dist/run/RunLock.d.ts +28 -2
- package/dist/run/RunLock.js +49 -7
- package/dist/run/RunManager.js +51 -3
- package/dist/run/RunQueue.d.ts +1 -0
- package/dist/run/RunQueue.js +8 -2
- package/dist/run/factory.d.ts +7 -0
- package/dist/run/factory.js +1 -0
- package/dist/run/index.d.ts +2 -2
- package/dist/run/index.js +1 -1
- package/dist/run/redirect-target.d.ts +7 -0
- package/dist/run/redirect-target.js +13 -0
- package/dist/run/types.js +1 -1
- package/dist/services/browser-open.d.ts +13 -0
- package/dist/services/browser-open.js +16 -0
- package/dist/services/dream-consolidation.d.ts +57 -0
- package/dist/services/dream-consolidation.js +151 -0
- package/dist/services/memory-orchestrator.js +18 -0
- package/dist/services/notifier.d.ts +8 -0
- package/dist/services/notifier.js +39 -25
- package/dist/services/oauth.js +5 -7
- package/dist/services/session-memory-sort.d.ts +8 -0
- package/dist/services/session-memory-sort.js +9 -0
- package/dist/services/session-memory.js +8 -9
- package/dist/session/file-history.js +10 -3
- package/dist/session/session-manager.d.ts +23 -1
- package/dist/session/session-manager.js +73 -2
- package/dist/settings/disk-defaults.d.ts +35 -0
- package/dist/settings/disk-defaults.js +24 -0
- package/dist/settings/manager.d.ts +33 -0
- package/dist/settings/manager.js +94 -1
- package/dist/settings/personalization.d.ts +19 -0
- package/dist/settings/personalization.js +7 -0
- package/dist/settings/schema.d.ts +1142 -126
- package/dist/settings/schema.js +119 -10
- package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
- package/dist/tool-system/builtin/add-marketplace.js +76 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +11 -0
- package/dist/tool-system/builtin/agent-registry.js +8 -4
- package/dist/tool-system/builtin/agent.d.ts +29 -10
- package/dist/tool-system/builtin/agent.js +108 -52
- package/dist/tool-system/builtin/apply-patch/index.js +22 -3
- package/dist/tool-system/builtin/arena.js +6 -7
- package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
- package/dist/tool-system/builtin/complete-goal.js +45 -0
- package/dist/tool-system/builtin/cron.js +53 -9
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +6 -1
- package/dist/tool-system/builtin/generate-image.d.ts +25 -0
- package/dist/tool-system/builtin/generate-image.js +146 -0
- package/dist/tool-system/builtin/glob.js +7 -0
- package/dist/tool-system/builtin/grep.js +6 -0
- package/dist/tool-system/builtin/index.d.ts +18 -1
- package/dist/tool-system/builtin/index.js +62 -1
- package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
- package/dist/tool-system/builtin/notebook-edit.js +8 -1
- package/dist/tool-system/builtin/read.d.ts +2 -1
- package/dist/tool-system/builtin/read.js +9 -1
- package/dist/tool-system/builtin/sleep.js +9 -3
- package/dist/tool-system/builtin/task.js +4 -1
- package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
- package/dist/tool-system/builtin/update-automation-memory.js +60 -0
- package/dist/tool-system/builtin/view-image.d.ts +18 -0
- package/dist/tool-system/builtin/view-image.js +96 -0
- package/dist/tool-system/builtin/web-search.d.ts +4 -1
- package/dist/tool-system/builtin/web-search.js +36 -3
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +7 -1
- package/dist/tool-system/context.d.ts +12 -0
- package/dist/tool-system/executor.d.ts +0 -1
- package/dist/tool-system/executor.js +34 -40
- package/dist/tool-system/investigation-guard.d.ts +4 -1
- package/dist/tool-system/investigation-guard.js +19 -3
- package/dist/tool-system/mcp-manager.d.ts +36 -12
- package/dist/tool-system/mcp-manager.js +173 -10
- package/dist/tool-system/path-policy.d.ts +94 -0
- package/dist/tool-system/path-policy.js +279 -0
- package/dist/tool-system/permission.js +43 -50
- package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
- package/dist/tool-system/plan-mode-allowlist.js +43 -0
- package/dist/tool-system/registry.js +12 -1
- package/dist/tool-system/validation.d.ts +10 -0
- package/dist/tool-system/validation.js +16 -3
- package/dist/types.d.ts +90 -15
- package/dist/utils/format.js +4 -2
- package/dist/utils/lockfile.js +9 -2
- package/dist/utils/theme.d.ts +6 -0
- package/dist/utils/theme.js +4 -2
- package/package.json +2 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History-image stripping for non-vision models.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: the vision gate in `Engine.run` only rejects *new* image
|
|
5
|
+
* attachments on the current turn. But a conversation can accumulate image
|
|
6
|
+
* content blocks while a vision-capable model is active, then the user
|
|
7
|
+
* switches to a non-vision model (e.g. DeepSeek) mid-session. Those
|
|
8
|
+
* *historical* image blocks are still in the transcript and get re-serialized
|
|
9
|
+
* on every subsequent request — OpenAI-compat backends 400 with
|
|
10
|
+
* `unknown variant 'image_url', expected 'text'`, which then falls back out
|
|
11
|
+
* of streaming and degrades the turn.
|
|
12
|
+
*
|
|
13
|
+
* Fix: when the active model has `supportsVision: false`, replace each
|
|
14
|
+
* message's image blocks with a single text placeholder before the request
|
|
15
|
+
* is built. The transcript/store is untouched — only the outgoing copy is
|
|
16
|
+
* sanitized — so switching back to a vision model still sees the images.
|
|
17
|
+
*
|
|
18
|
+
* Pure function, no I/O. Tested in `strip-vision.test.ts`.
|
|
19
|
+
*/
|
|
20
|
+
import type { Message } from "../types.js";
|
|
21
|
+
/** Text we substitute for an elided image block. Surfaced to the model so it
|
|
22
|
+
* knows an image *was* here (vs silently vanishing the context). */
|
|
23
|
+
export declare const VISION_PLACEHOLDER = "[\u56FE\u7247\u5DF2\u7701\u7565:\u5F53\u524D\u6A21\u578B\u4E0D\u652F\u6301\u89C6\u89C9\u8F93\u5165]";
|
|
24
|
+
/**
|
|
25
|
+
* Return a copy of `messages` with image content blocks replaced by a text
|
|
26
|
+
* placeholder, but only when `supportsVision` is false.
|
|
27
|
+
*
|
|
28
|
+
* Identity-preserving: when vision is supported, or no message carries an
|
|
29
|
+
* image, the original array (and untouched message objects) are returned as-is
|
|
30
|
+
* so callers pay nothing on the common path.
|
|
31
|
+
*/
|
|
32
|
+
export declare function stripVisionFromHistory(messages: Message[], supportsVision: boolean): Message[];
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History-image stripping for non-vision models.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: the vision gate in `Engine.run` only rejects *new* image
|
|
5
|
+
* attachments on the current turn. But a conversation can accumulate image
|
|
6
|
+
* content blocks while a vision-capable model is active, then the user
|
|
7
|
+
* switches to a non-vision model (e.g. DeepSeek) mid-session. Those
|
|
8
|
+
* *historical* image blocks are still in the transcript and get re-serialized
|
|
9
|
+
* on every subsequent request — OpenAI-compat backends 400 with
|
|
10
|
+
* `unknown variant 'image_url', expected 'text'`, which then falls back out
|
|
11
|
+
* of streaming and degrades the turn.
|
|
12
|
+
*
|
|
13
|
+
* Fix: when the active model has `supportsVision: false`, replace each
|
|
14
|
+
* message's image blocks with a single text placeholder before the request
|
|
15
|
+
* is built. The transcript/store is untouched — only the outgoing copy is
|
|
16
|
+
* sanitized — so switching back to a vision model still sees the images.
|
|
17
|
+
*
|
|
18
|
+
* Pure function, no I/O. Tested in `strip-vision.test.ts`.
|
|
19
|
+
*/
|
|
20
|
+
/** Text we substitute for an elided image block. Surfaced to the model so it
|
|
21
|
+
* knows an image *was* here (vs silently vanishing the context). */
|
|
22
|
+
export const VISION_PLACEHOLDER = "[图片已省略:当前模型不支持视觉输入]";
|
|
23
|
+
/**
|
|
24
|
+
* Return a copy of `messages` with image content blocks replaced by a text
|
|
25
|
+
* placeholder, but only when `supportsVision` is false.
|
|
26
|
+
*
|
|
27
|
+
* Identity-preserving: when vision is supported, or no message carries an
|
|
28
|
+
* image, the original array (and untouched message objects) are returned as-is
|
|
29
|
+
* so callers pay nothing on the common path.
|
|
30
|
+
*/
|
|
31
|
+
export function stripVisionFromHistory(messages, supportsVision) {
|
|
32
|
+
if (supportsVision)
|
|
33
|
+
return messages;
|
|
34
|
+
let changed = false;
|
|
35
|
+
const out = messages.map((msg) => {
|
|
36
|
+
if (typeof msg.content === "string")
|
|
37
|
+
return msg;
|
|
38
|
+
// Need to process if there's a top-level image, OR a tool_result whose
|
|
39
|
+
// own content array carries a nested image (view_image-produced screenshots
|
|
40
|
+
// live in `tool_result.content: ContentBlock[]`).
|
|
41
|
+
if (!msg.content.some(needsStrip))
|
|
42
|
+
return msg;
|
|
43
|
+
changed = true;
|
|
44
|
+
const rebuilt = [];
|
|
45
|
+
let placeholderInserted = false;
|
|
46
|
+
for (const block of msg.content) {
|
|
47
|
+
if (block.type === "image") {
|
|
48
|
+
// Collapse runs of images into a single placeholder per message —
|
|
49
|
+
// five screenshots don't need five identical notes.
|
|
50
|
+
if (!placeholderInserted) {
|
|
51
|
+
rebuilt.push({ type: "text", text: VISION_PLACEHOLDER });
|
|
52
|
+
placeholderInserted = true;
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (block.type === "tool_result" &&
|
|
57
|
+
Array.isArray(block.content) &&
|
|
58
|
+
block.content.some((b) => b.type === "image")) {
|
|
59
|
+
rebuilt.push({ ...block, content: stripImageBlocks(block.content) });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
rebuilt.push(block);
|
|
63
|
+
}
|
|
64
|
+
return { ...msg, content: rebuilt };
|
|
65
|
+
});
|
|
66
|
+
return changed ? out : messages;
|
|
67
|
+
}
|
|
68
|
+
/** True if this block holds an image we must elide — either it *is* an image,
|
|
69
|
+
* or it's a tool_result whose nested content array contains one. */
|
|
70
|
+
function needsStrip(block) {
|
|
71
|
+
if (block.type === "image")
|
|
72
|
+
return true;
|
|
73
|
+
return (block.type === "tool_result" &&
|
|
74
|
+
Array.isArray(block.content) &&
|
|
75
|
+
block.content.some((b) => b.type === "image"));
|
|
76
|
+
}
|
|
77
|
+
/** Replace image blocks in a ContentBlock[] with a single text placeholder
|
|
78
|
+
* (consecutive/multiple images collapse to one), preserving other blocks.
|
|
79
|
+
* Used for both the message top level and nested tool_result.content. */
|
|
80
|
+
function stripImageBlocks(blocks) {
|
|
81
|
+
const rebuilt = [];
|
|
82
|
+
let placeholderInserted = false;
|
|
83
|
+
for (const block of blocks) {
|
|
84
|
+
if (block.type === "image") {
|
|
85
|
+
if (!placeholderInserted) {
|
|
86
|
+
rebuilt.push({ type: "text", text: VISION_PLACEHOLDER });
|
|
87
|
+
placeholderInserted = true;
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
rebuilt.push(block);
|
|
92
|
+
}
|
|
93
|
+
return rebuilt;
|
|
94
|
+
}
|
package/dist/llm/types.d.ts
CHANGED
|
@@ -20,13 +20,12 @@ export interface CreateMessageOptions {
|
|
|
20
20
|
*/
|
|
21
21
|
recordUsage?: boolean;
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* other compatible endpoints.
|
|
23
|
+
* Reasoning/thinking setting for this call. Overrides LLMConfig.reasoning.
|
|
24
|
+
* Rich shape ({mode:"off"|"on"} | {mode:"effort",effort} |
|
|
25
|
+
* {mode:"budget",budgetTokens}) — translated to the per-vendor wire form by
|
|
26
|
+
* the client's capability layer.
|
|
28
27
|
*/
|
|
29
|
-
|
|
28
|
+
reasoning?: import("./reasoning-setting.js").ReasoningSetting;
|
|
30
29
|
}
|
|
31
30
|
export interface LLMUsageTracker {
|
|
32
31
|
records: TokenUsage[];
|
package/dist/logging/logger.js
CHANGED
|
@@ -25,6 +25,7 @@ import { appendFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, readFil
|
|
|
25
25
|
import { join, dirname } from "node:path";
|
|
26
26
|
import { homedir } from "node:os";
|
|
27
27
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
28
|
+
import { redactSecrets } from "./sanitize-messages.js";
|
|
28
29
|
const LEVEL_ORDER = {
|
|
29
30
|
debug: 0,
|
|
30
31
|
info: 1,
|
|
@@ -246,7 +247,14 @@ class Logger {
|
|
|
246
247
|
recordError(msg, data) {
|
|
247
248
|
if (inMemoryErrors.length >= MAX_IN_MEMORY_ERRORS)
|
|
248
249
|
inMemoryErrors.shift();
|
|
249
|
-
|
|
250
|
+
// Scrub secrets out of msg + data before parking the entry in the in-
|
|
251
|
+
// memory ring. The ring is exposed via diagnostics endpoints, so
|
|
252
|
+
// anything that survives here is reachable by protocol clients.
|
|
253
|
+
inMemoryErrors.push({
|
|
254
|
+
msg: redactSecrets(msg),
|
|
255
|
+
t: new Date().toISOString(),
|
|
256
|
+
data: data ? redactSecrets(data) : undefined,
|
|
257
|
+
});
|
|
250
258
|
}
|
|
251
259
|
write(level, msg, data) {
|
|
252
260
|
if (!this.enabled)
|
|
@@ -262,7 +270,13 @@ class Logger {
|
|
|
262
270
|
return;
|
|
263
271
|
}
|
|
264
272
|
const t = new Date().toISOString();
|
|
265
|
-
|
|
273
|
+
// Scrub msg + data before the entry is built. msg is a plain string so
|
|
274
|
+
// only bearer-tokens / URL secret query params can hide there; data is
|
|
275
|
+
// arbitrary so it gets the full recursive walk. Done at the boundary so
|
|
276
|
+
// every level/category sink shares the same redaction rules.
|
|
277
|
+
const safeMsg = redactSecrets(msg);
|
|
278
|
+
const safeData = data ? redactSecrets(data) : undefined;
|
|
279
|
+
const entry = { t, l: level, msg: safeMsg, ...this.context };
|
|
266
280
|
if (cat && !entry.cat)
|
|
267
281
|
entry.cat = cat;
|
|
268
282
|
// Stamp the current process-wide session id unless this logger's
|
|
@@ -270,8 +284,8 @@ class Logger {
|
|
|
270
284
|
// children created before Engine.run() still pick up the real sid.
|
|
271
285
|
if (entry.sid === undefined)
|
|
272
286
|
entry.sid = getCurrentSid();
|
|
273
|
-
if (
|
|
274
|
-
entry.d =
|
|
287
|
+
if (safeData)
|
|
288
|
+
entry.d = safeData;
|
|
275
289
|
const line = JSON.stringify(entry) + "\n";
|
|
276
290
|
if (this.stderrEcho) {
|
|
277
291
|
process.stderr.write(line);
|
|
@@ -32,6 +32,16 @@ export declare function sanitizeContent(content: Message["content"]): Message["c
|
|
|
32
32
|
* path for the overwhelmingly common pure-text case).
|
|
33
33
|
*/
|
|
34
34
|
export declare function sanitizeMessages(messages: readonly Message[]): Message[];
|
|
35
|
+
/**
|
|
36
|
+
* Recursively walk a value and return a *new* value with secret-looking
|
|
37
|
+
* fields replaced by "[redacted]". Used by the logger (so anything ending up
|
|
38
|
+
* in `entry.d` is scrubbed) and by diagnostics / in-memory error capture.
|
|
39
|
+
*
|
|
40
|
+
* Pure / no I/O. Does not mutate the input — important because the same
|
|
41
|
+
* object may also be on its way to a transcript or UI stream where the
|
|
42
|
+
* unredacted form is correct.
|
|
43
|
+
*/
|
|
44
|
+
export declare function redactSecrets<T>(value: T, depth?: number): T;
|
|
35
45
|
/**
|
|
36
46
|
* Sanitize a free-form task string before logging.
|
|
37
47
|
*
|
|
@@ -129,6 +129,96 @@ export function sanitizeMessages(messages) {
|
|
|
129
129
|
}
|
|
130
130
|
return touched ? out : messages;
|
|
131
131
|
}
|
|
132
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
133
|
+
// Secret redaction
|
|
134
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
135
|
+
//
|
|
136
|
+
// Beyond image payloads, log/diagnostics entries can carry API keys,
|
|
137
|
+
// authorization headers, provider tokens, and similar secrets via
|
|
138
|
+
// `data` blobs (settings dumps, provider configs, error responses, …).
|
|
139
|
+
// These walkers redact secrets *in place on a deep clone* so the caller
|
|
140
|
+
// can still log "useful shape, scrubbed values".
|
|
141
|
+
const SECRET_KEY_RE = /(^|[._-])(api[_-]?key|authorization|x[_-]api[_-]key|bearer[_-]?token|access[_-]?token|refresh[_-]?token|session[_-]?token|token|secret|password|client[_-]?secret|cookie)($|[._-])/i;
|
|
142
|
+
const BEARER_RE = /\bBearer\s+[A-Za-z0-9._\-+/=~]{8,}/g;
|
|
143
|
+
// URL query parameters that look like credentials. Conservative — only
|
|
144
|
+
// trigger on a small named set so a normal `?id=…&q=…` URL passes through.
|
|
145
|
+
const URL_SECRET_QS_RE = /([?&](?:key|api_key|apikey|access_token|token|auth|sig|signature)=)[^&#\s]+/gi;
|
|
146
|
+
const REDACTED = "[redacted]";
|
|
147
|
+
const MAX_DEPTH = 10;
|
|
148
|
+
function isSecretKey(key) {
|
|
149
|
+
return SECRET_KEY_RE.test(key);
|
|
150
|
+
}
|
|
151
|
+
/** Scrub bare `Bearer <token>` and credential-looking URL query params from a string. */
|
|
152
|
+
function redactSecretsInString(s) {
|
|
153
|
+
let out = s;
|
|
154
|
+
if (BEARER_RE.test(out))
|
|
155
|
+
out = out.replace(BEARER_RE, "Bearer [redacted]");
|
|
156
|
+
if (URL_SECRET_QS_RE.test(out))
|
|
157
|
+
out = out.replace(URL_SECRET_QS_RE, "$1[redacted]");
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Recursively walk a value and return a *new* value with secret-looking
|
|
162
|
+
* fields replaced by "[redacted]". Used by the logger (so anything ending up
|
|
163
|
+
* in `entry.d` is scrubbed) and by diagnostics / in-memory error capture.
|
|
164
|
+
*
|
|
165
|
+
* Pure / no I/O. Does not mutate the input — important because the same
|
|
166
|
+
* object may also be on its way to a transcript or UI stream where the
|
|
167
|
+
* unredacted form is correct.
|
|
168
|
+
*/
|
|
169
|
+
export function redactSecrets(value, depth = 0) {
|
|
170
|
+
if (value === null || value === undefined)
|
|
171
|
+
return value;
|
|
172
|
+
if (depth > MAX_DEPTH)
|
|
173
|
+
return value;
|
|
174
|
+
const t = typeof value;
|
|
175
|
+
if (t === "string")
|
|
176
|
+
return redactSecretsInString(value);
|
|
177
|
+
if (t === "number" || t === "boolean" || t === "bigint" || t === "symbol")
|
|
178
|
+
return value;
|
|
179
|
+
if (t === "function")
|
|
180
|
+
return value;
|
|
181
|
+
if (Array.isArray(value)) {
|
|
182
|
+
return value.map((v) => redactSecrets(v, depth + 1));
|
|
183
|
+
}
|
|
184
|
+
// Error objects: preserve name/message/stack shape but redact within.
|
|
185
|
+
if (value instanceof Error) {
|
|
186
|
+
const clone = {
|
|
187
|
+
name: value.name,
|
|
188
|
+
message: redactSecretsInString(value.message),
|
|
189
|
+
...(value.stack ? { stack: redactSecretsInString(value.stack) } : {}),
|
|
190
|
+
};
|
|
191
|
+
// Some libraries attach extra properties (`.response`, `.config`, …) —
|
|
192
|
+
// walk them as well.
|
|
193
|
+
for (const key of Object.keys(value)) {
|
|
194
|
+
if (key === "name" || key === "message" || key === "stack")
|
|
195
|
+
continue;
|
|
196
|
+
const v = value[key];
|
|
197
|
+
clone[key] = isSecretKey(key) ? REDACTED : redactSecrets(v, depth + 1);
|
|
198
|
+
}
|
|
199
|
+
return clone;
|
|
200
|
+
}
|
|
201
|
+
if (t === "object") {
|
|
202
|
+
const obj = value;
|
|
203
|
+
const out = {};
|
|
204
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
205
|
+
if (isSecretKey(k)) {
|
|
206
|
+
// Preserve presence (null/undefined stay as-is so consumers can
|
|
207
|
+
// still distinguish "key present" vs "key absent"; non-empty values
|
|
208
|
+
// collapse to [redacted]).
|
|
209
|
+
if (v === null || v === undefined || v === "")
|
|
210
|
+
out[k] = v;
|
|
211
|
+
else
|
|
212
|
+
out[k] = REDACTED;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
out[k] = redactSecrets(v, depth + 1);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
219
|
+
}
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
132
222
|
/**
|
|
133
223
|
* Sanitize a free-form task string before logging.
|
|
134
224
|
*
|
package/dist/lsp/client.js
CHANGED
|
@@ -10,7 +10,12 @@ export class LSPClient extends EventEmitter {
|
|
|
10
10
|
process;
|
|
11
11
|
nextId = 1;
|
|
12
12
|
pending = new Map();
|
|
13
|
-
buffer
|
|
13
|
+
// The LSP framing uses byte-counted Content-Length, so we buffer bytes,
|
|
14
|
+
// not characters. Joining chunks as strings (`data.toString()` + string
|
|
15
|
+
// concat) can split a multibyte UTF-8 codepoint across chunks and de-
|
|
16
|
+
// synchronize against Content-Length. Stay in Buffer-land until we've
|
|
17
|
+
// sliced out exactly the right body length.
|
|
18
|
+
buffer = Buffer.alloc(0);
|
|
14
19
|
contentLength = -1;
|
|
15
20
|
initialized = false;
|
|
16
21
|
constructor(command, args = [], cwd) {
|
|
@@ -24,7 +29,7 @@ export class LSPClient extends EventEmitter {
|
|
|
24
29
|
cwd: this.cwd,
|
|
25
30
|
stdio: ["pipe", "pipe", "pipe"],
|
|
26
31
|
});
|
|
27
|
-
this.process.stdout?.on("data", (data) => this.handleData(data
|
|
32
|
+
this.process.stdout?.on("data", (data) => this.handleData(data));
|
|
28
33
|
this.process.stderr?.on("data", (_data) => {
|
|
29
34
|
// LSP servers often log to stderr — ignore
|
|
30
35
|
});
|
|
@@ -116,26 +121,41 @@ export class LSPClient extends EventEmitter {
|
|
|
116
121
|
return this.process !== undefined && !this.process.killed;
|
|
117
122
|
}
|
|
118
123
|
handleData(data) {
|
|
119
|
-
|
|
124
|
+
// Append the new chunk as bytes. Buffer.concat avoids the UTF-8
|
|
125
|
+
// boundary corruption that string concatenation produces — a
|
|
126
|
+
// multibyte codepoint split across two TCP/pipe chunks decodes to
|
|
127
|
+
// U+FFFD, mangles the buffer length count, and breaks framing.
|
|
128
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
120
129
|
while (true) {
|
|
121
130
|
if (this.contentLength < 0) {
|
|
131
|
+
// Search for the header terminator in bytes, not chars.
|
|
122
132
|
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
123
133
|
if (headerEnd < 0)
|
|
124
134
|
break;
|
|
125
|
-
|
|
135
|
+
// Headers are ASCII per spec, so toString("ascii") is safe and
|
|
136
|
+
// avoids paying for a UTF-8 decode on the framing band.
|
|
137
|
+
const header = this.buffer.slice(0, headerEnd).toString("ascii");
|
|
126
138
|
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
127
139
|
if (!match) {
|
|
128
|
-
this.buffer = this.buffer.
|
|
140
|
+
this.buffer = this.buffer.subarray(headerEnd + 4);
|
|
129
141
|
continue;
|
|
130
142
|
}
|
|
131
143
|
this.contentLength = parseInt(match[1], 10);
|
|
132
|
-
this.buffer = this.buffer.
|
|
144
|
+
this.buffer = this.buffer.subarray(headerEnd + 4);
|
|
133
145
|
}
|
|
146
|
+
// Content-Length is a BYTE count — compare against the buffer's
|
|
147
|
+
// byte length, not its character length. Pre-fix this compared
|
|
148
|
+
// string lengths; with multibyte responses (e.g. CJK identifiers,
|
|
149
|
+
// emoji in diagnostics) we'd block waiting for bytes that had
|
|
150
|
+
// already arrived.
|
|
134
151
|
if (this.buffer.length < this.contentLength)
|
|
135
152
|
break;
|
|
136
|
-
const
|
|
137
|
-
this.buffer = this.buffer.
|
|
153
|
+
const bodyBytes = this.buffer.subarray(0, this.contentLength);
|
|
154
|
+
this.buffer = this.buffer.subarray(this.contentLength);
|
|
138
155
|
this.contentLength = -1;
|
|
156
|
+
// Decode the full body in one shot — at this point we have the
|
|
157
|
+
// exact byte count, so the UTF-8 boundary issue is fully gone.
|
|
158
|
+
const content = bodyBytes.toString("utf8");
|
|
139
159
|
try {
|
|
140
160
|
const message = JSON.parse(content);
|
|
141
161
|
if ("id" in message && this.pending.has(message.id)) {
|
package/dist/lsp/manager.js
CHANGED
|
@@ -6,6 +6,7 @@ import { LSPClient } from "./client.js";
|
|
|
6
6
|
import { BUILTIN_LSP_SERVERS } from "./servers.js";
|
|
7
7
|
import { execSync } from "node:child_process";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { rootUriToPath } from "./root-path.js";
|
|
9
10
|
export class LSPServerManager {
|
|
10
11
|
servers = new Map();
|
|
11
12
|
rootUri;
|
|
@@ -49,7 +50,7 @@ export class LSPServerManager {
|
|
|
49
50
|
}
|
|
50
51
|
managed.state = "starting";
|
|
51
52
|
try {
|
|
52
|
-
const client = new LSPClient(managed.config.command, managed.config.args, this.rootUri
|
|
53
|
+
const client = new LSPClient(managed.config.command, managed.config.args, rootUriToPath(this.rootUri));
|
|
53
54
|
await client.start();
|
|
54
55
|
await client.initialize(this.rootUri);
|
|
55
56
|
managed.client = client;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a `file://` root URI to a platform filesystem path.
|
|
3
|
+
*
|
|
4
|
+
* The LSP manager used `rootUri.replace("file://", "")`, which only strips the
|
|
5
|
+
* first occurrence and leaves percent-encoding intact, and on Windows turns
|
|
6
|
+
* `file:///C:/x` into the invalid `/C:/x`. fileURLToPath does the proper
|
|
7
|
+
* platform conversion (drive letters, percent-decoding, UNC).
|
|
8
|
+
*/
|
|
9
|
+
export declare function rootUriToPath(rootUri: string): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
/**
|
|
3
|
+
* Convert a `file://` root URI to a platform filesystem path.
|
|
4
|
+
*
|
|
5
|
+
* The LSP manager used `rootUri.replace("file://", "")`, which only strips the
|
|
6
|
+
* first occurrence and leaves percent-encoding intact, and on Windows turns
|
|
7
|
+
* `file:///C:/x` into the invalid `/C:/x`. fileURLToPath does the proper
|
|
8
|
+
* platform conversion (drive letters, percent-decoding, UNC).
|
|
9
|
+
*/
|
|
10
|
+
export function rootUriToPath(rootUri) {
|
|
11
|
+
return fileURLToPath(rootUri);
|
|
12
|
+
}
|
package/dist/onboarding.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* (see src/ui/components/OnboardingPrompt.tsx); this module only exposes
|
|
5
5
|
* pure(-ish) helpers it consumes, so there's a single input stack.
|
|
6
6
|
*/
|
|
7
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync, renameSync } from "node:fs";
|
|
7
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, renameSync, rmSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import {
|
|
9
|
+
import { userHome } from "./settings/manager.js";
|
|
10
10
|
import { getOpenRouterModels } from "./data/openrouter-models.js";
|
|
11
11
|
import { sanitizeApiKey } from "./llm/api-key-sanitize.js";
|
|
12
12
|
export const PROVIDERS = [
|
|
@@ -257,8 +257,13 @@ export function resolveApiKey(optionsApiKey, settingsApiKey) {
|
|
|
257
257
|
if (!p.envKey)
|
|
258
258
|
continue;
|
|
259
259
|
const v = process.env[p.envKey];
|
|
260
|
-
if (v
|
|
261
|
-
|
|
260
|
+
if (!v)
|
|
261
|
+
continue;
|
|
262
|
+
// Same sanitization boundary as detectEnvKeys — env-sourced keys carry
|
|
263
|
+
// CRLF/BOM/zero-width/control chars; .trim() alone misses interior ones.
|
|
264
|
+
const cleaned = sanitizeApiKey(v).value;
|
|
265
|
+
if (cleaned)
|
|
266
|
+
return cleaned;
|
|
262
267
|
}
|
|
263
268
|
return undefined;
|
|
264
269
|
}
|
|
@@ -272,7 +277,7 @@ export function resolveApiKey(optionsApiKey, settingsApiKey) {
|
|
|
272
277
|
* then settings.model (legacy top-level). undefined when nothing matches.
|
|
273
278
|
*/
|
|
274
279
|
export function findSavedKeyForProvider(provider) {
|
|
275
|
-
const file = join(
|
|
280
|
+
const file = join(userHome(), ".code-shell", "settings.json");
|
|
276
281
|
if (!existsSync(file))
|
|
277
282
|
return undefined;
|
|
278
283
|
let data;
|
|
@@ -304,7 +309,7 @@ export function findSavedKeyForProvider(provider) {
|
|
|
304
309
|
* just the provider's first model.
|
|
305
310
|
*/
|
|
306
311
|
export function loadSavedModelsForProvider(provider) {
|
|
307
|
-
const file = join(
|
|
312
|
+
const file = join(userHome(), ".code-shell", "settings.json");
|
|
308
313
|
if (!existsSync(file))
|
|
309
314
|
return [];
|
|
310
315
|
let data;
|
|
@@ -334,7 +339,7 @@ export function hasApiKey() {
|
|
|
334
339
|
// /logout look like a no-op when the others still held credentials.
|
|
335
340
|
// Reads ~/.code-shell/ only — ~/.claude/ compat was dropped because Claude
|
|
336
341
|
// Code's settings schema diverges and merging broke boot.
|
|
337
|
-
const p = join(
|
|
342
|
+
const p = join(userHome(), ".code-shell", "settings.json");
|
|
338
343
|
if (existsSync(p)) {
|
|
339
344
|
try {
|
|
340
345
|
const data = JSON.parse(readFileSync(p, "utf-8"));
|
|
@@ -501,7 +506,7 @@ export function modelDisplayName(model) {
|
|
|
501
506
|
return base.charAt(0).toUpperCase() + base.slice(1);
|
|
502
507
|
}
|
|
503
508
|
export function saveSettings(result, providerDef, poolModels) {
|
|
504
|
-
const dir = join(
|
|
509
|
+
const dir = join(userHome(), ".code-shell");
|
|
505
510
|
const file = join(dir, "settings.json");
|
|
506
511
|
mkdirSync(dir, { recursive: true });
|
|
507
512
|
let existing = {};
|
|
@@ -551,7 +556,7 @@ export function saveSettings(result, providerDef, poolModels) {
|
|
|
551
556
|
* Writes atomically via tmp+rename.
|
|
552
557
|
*/
|
|
553
558
|
export function appendOnboardingResult(opts) {
|
|
554
|
-
const dir = join(
|
|
559
|
+
const dir = join(userHome(), ".code-shell");
|
|
555
560
|
const file = join(dir, "settings.json");
|
|
556
561
|
mkdirSync(dir, { recursive: true });
|
|
557
562
|
let existing = {};
|
|
@@ -602,13 +607,15 @@ export function appendOnboardingResult(opts) {
|
|
|
602
607
|
renameSync(tmp, file);
|
|
603
608
|
}
|
|
604
609
|
catch {
|
|
605
|
-
// Fallback: best-effort direct write if rename fails (e.g. cross-device)
|
|
610
|
+
// Fallback: best-effort direct write if rename fails (e.g. cross-device),
|
|
611
|
+
// then remove the orphaned temp file the failed rename left behind.
|
|
606
612
|
writeFileSync(file, JSON.stringify(updated, null, 2) + "\n", "utf-8");
|
|
613
|
+
rmSync(tmp, { force: true });
|
|
607
614
|
}
|
|
608
615
|
}
|
|
609
616
|
// saveArenaSettings removed — replaced by saveArenaSettingsByKeys
|
|
610
617
|
export function saveArenaSettingsByKeys(keys) {
|
|
611
|
-
const dir = join(
|
|
618
|
+
const dir = join(userHome(), ".code-shell");
|
|
612
619
|
const file = join(dir, "settings.json");
|
|
613
620
|
let existing = {};
|
|
614
621
|
if (existsSync(file)) {
|
package/dist/plugins/gitOps.d.ts
CHANGED
|
@@ -21,9 +21,43 @@ export type GitResult = {
|
|
|
21
21
|
ok: false;
|
|
22
22
|
error: string;
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Force git to never prompt interactively. gitOps runs in headless contexts
|
|
26
|
+
* (the Electron main process, the agent worker) that have no TTY, so a
|
|
27
|
+
* private/auth-required clone or an unknown SSH host key would otherwise make
|
|
28
|
+
* git BLOCK on a credential/host-key prompt — the operation appears to hang
|
|
29
|
+
* until the safeSpawn timeout (or an off-screen askpass dialog). With these
|
|
30
|
+
* set, git fails fast with an auth error instead.
|
|
31
|
+
*
|
|
32
|
+
* - GIT_TERMINAL_PROMPT=0 — git's own username/password prompt becomes an error.
|
|
33
|
+
* - GIT_SSH_COMMAND BatchMode=yes — ssh never asks for a password/passphrase;
|
|
34
|
+
* accept-new auto-trusts a first-seen host key (instead of the y/n prompt)
|
|
35
|
+
* but still rejects a changed key. Not overridden if the caller already
|
|
36
|
+
* provides GIT_SSH_COMMAND (e.g. a custom identity file).
|
|
37
|
+
*/
|
|
38
|
+
export declare function nonInteractiveGitEnv(base: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
39
|
+
/**
|
|
40
|
+
* Clone a marketplace repo cheaply. These repos can bundle hundreds of plugins
|
|
41
|
+
* (thousands of files), but adding a marketplace only needs its manifest — the
|
|
42
|
+
* individual plugin trees are pulled on demand at install time
|
|
43
|
+
* ({@link gitSparseCheckoutAdd}). So we do a blobless, no-checkout clone and
|
|
44
|
+
* then sparse-check out only the manifest directories.
|
|
45
|
+
*
|
|
46
|
+
* `sparsePaths` defaults to the two manifest locations we support
|
|
47
|
+
* (.claude-plugin and .agents/plugins). On any failure of the sparse setup we
|
|
48
|
+
* fall back to checking out the full tree, so correctness never depends on
|
|
49
|
+
* sparse support being present.
|
|
50
|
+
*/
|
|
24
51
|
export declare function gitClone(url: string, destDir: string, options?: {
|
|
25
52
|
ref?: string;
|
|
53
|
+
sparsePaths?: string[];
|
|
26
54
|
}): Promise<GitResult>;
|
|
55
|
+
/**
|
|
56
|
+
* Expand a sparse-checkout to include an additional path (a plugin subdir),
|
|
57
|
+
* materializing its blobs. Best-effort: if the repo isn't sparse this errors
|
|
58
|
+
* harmlessly and the caller proceeds (the files are already present).
|
|
59
|
+
*/
|
|
60
|
+
export declare function gitSparseCheckoutAdd(repoDir: string, relPath: string): Promise<GitResult>;
|
|
27
61
|
export declare function gitRevParseHead(repoDir: string): Promise<GitResult>;
|
|
28
62
|
export declare function gitFetchAndReset(repoDir: string, ref?: string): Promise<GitResult>;
|
|
29
63
|
/**
|
package/dist/plugins/gitOps.js
CHANGED
|
@@ -15,10 +15,31 @@
|
|
|
15
15
|
* classification. See docs/architecture/17 for the trust framing.
|
|
16
16
|
*/
|
|
17
17
|
import { safeSpawn } from "../runtime/safe-spawn.js";
|
|
18
|
+
/**
|
|
19
|
+
* Force git to never prompt interactively. gitOps runs in headless contexts
|
|
20
|
+
* (the Electron main process, the agent worker) that have no TTY, so a
|
|
21
|
+
* private/auth-required clone or an unknown SSH host key would otherwise make
|
|
22
|
+
* git BLOCK on a credential/host-key prompt — the operation appears to hang
|
|
23
|
+
* until the safeSpawn timeout (or an off-screen askpass dialog). With these
|
|
24
|
+
* set, git fails fast with an auth error instead.
|
|
25
|
+
*
|
|
26
|
+
* - GIT_TERMINAL_PROMPT=0 — git's own username/password prompt becomes an error.
|
|
27
|
+
* - GIT_SSH_COMMAND BatchMode=yes — ssh never asks for a password/passphrase;
|
|
28
|
+
* accept-new auto-trusts a first-seen host key (instead of the y/n prompt)
|
|
29
|
+
* but still rejects a changed key. Not overridden if the caller already
|
|
30
|
+
* provides GIT_SSH_COMMAND (e.g. a custom identity file).
|
|
31
|
+
*/
|
|
32
|
+
export function nonInteractiveGitEnv(base) {
|
|
33
|
+
return {
|
|
34
|
+
...base,
|
|
35
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
36
|
+
GIT_SSH_COMMAND: base.GIT_SSH_COMMAND ?? "ssh -oBatchMode=yes -oStrictHostKeyChecking=accept-new",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
18
39
|
async function runGit(args, cwd, timeoutMs = 60_000) {
|
|
19
40
|
const r = await safeSpawn("git", args, {
|
|
20
41
|
cwd: cwd ?? process.cwd(),
|
|
21
|
-
env: process.env,
|
|
42
|
+
env: nonInteractiveGitEnv(process.env),
|
|
22
43
|
timeoutMs,
|
|
23
44
|
});
|
|
24
45
|
if (r.spawnFailed) {
|
|
@@ -35,12 +56,47 @@ async function runGit(args, cwd, timeoutMs = 60_000) {
|
|
|
35
56
|
error: `git ${args.join(" ")} exited ${r.exitCode}: ${r.stderr.trim() || "unknown error"}`,
|
|
36
57
|
};
|
|
37
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Clone a marketplace repo cheaply. These repos can bundle hundreds of plugins
|
|
61
|
+
* (thousands of files), but adding a marketplace only needs its manifest — the
|
|
62
|
+
* individual plugin trees are pulled on demand at install time
|
|
63
|
+
* ({@link gitSparseCheckoutAdd}). So we do a blobless, no-checkout clone and
|
|
64
|
+
* then sparse-check out only the manifest directories.
|
|
65
|
+
*
|
|
66
|
+
* `sparsePaths` defaults to the two manifest locations we support
|
|
67
|
+
* (.claude-plugin and .agents/plugins). On any failure of the sparse setup we
|
|
68
|
+
* fall back to checking out the full tree, so correctness never depends on
|
|
69
|
+
* sparse support being present.
|
|
70
|
+
*/
|
|
38
71
|
export async function gitClone(url, destDir, options) {
|
|
39
|
-
const
|
|
72
|
+
const sparsePaths = options?.sparsePaths ?? [".claude-plugin", ".agents/plugins"];
|
|
73
|
+
const args = ["clone", "--depth", "1", "--filter=blob:none", "--no-checkout"];
|
|
40
74
|
if (options?.ref)
|
|
41
75
|
args.push("--branch", options.ref);
|
|
42
76
|
args.push("--", url, destDir);
|
|
43
|
-
|
|
77
|
+
const cloned = await runGit(args);
|
|
78
|
+
if (!cloned.ok)
|
|
79
|
+
return cloned;
|
|
80
|
+
// Restrict the working tree to the manifest dirs, then check out. Use
|
|
81
|
+
// non-cone mode so nested paths like ".agents/plugins" match exactly.
|
|
82
|
+
const init = await runGit(["sparse-checkout", "set", "--no-cone", ...sparsePaths], destDir);
|
|
83
|
+
if (!init.ok) {
|
|
84
|
+
// Sparse unsupported/failed — fall back to a normal full checkout so the
|
|
85
|
+
// manifest is still present.
|
|
86
|
+
return runGit(["checkout"], destDir);
|
|
87
|
+
}
|
|
88
|
+
const checkout = await runGit(["checkout"], destDir);
|
|
89
|
+
if (!checkout.ok)
|
|
90
|
+
return checkout;
|
|
91
|
+
return cloned;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Expand a sparse-checkout to include an additional path (a plugin subdir),
|
|
95
|
+
* materializing its blobs. Best-effort: if the repo isn't sparse this errors
|
|
96
|
+
* harmlessly and the caller proceeds (the files are already present).
|
|
97
|
+
*/
|
|
98
|
+
export async function gitSparseCheckoutAdd(repoDir, relPath) {
|
|
99
|
+
return runGit(["sparse-checkout", "add", relPath], repoDir);
|
|
44
100
|
}
|
|
45
101
|
export async function gitRevParseHead(repoDir) {
|
|
46
102
|
return runGit(["rev-parse", "HEAD"], repoDir);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert one Codex agent TOML into a CC agent Markdown string.
|
|
3
|
+
* Real CC fields: name, description, model, body(=developer_instructions).
|
|
4
|
+
* Every other field is preserved with a `codex_` prefix (v1 inert; see spec §7.1).
|
|
5
|
+
* `mcp_servers` values are rewritten to `<plugin>:<server>` to match the MCP merge key.
|
|
6
|
+
*/
|
|
7
|
+
export declare function convertCodexAgentToml(toml: string, sourceName: string, pluginName: string): string;
|