peaks-cli 1.3.2 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/src/cli/commands/core-artifact-commands.js +6 -3
- package/dist/src/cli/commands/gate-commands.js +28 -19
- package/dist/src/cli/commands/hook-handle.d.ts +17 -0
- package/dist/src/cli/commands/hook-handle.js +111 -0
- package/dist/src/cli/commands/hooks-commands.js +72 -21
- package/dist/src/cli/commands/progress-commands.js +9 -2
- package/dist/src/cli/commands/progress-start-spawn.js +30 -4
- package/dist/src/cli/commands/project-commands.js +8 -4
- package/dist/src/cli/commands/statusline-commands.js +75 -17
- package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
- package/dist/src/cli/commands/sub-agent-commands.js +488 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
- package/dist/src/cli/commands/workflow-commands.js +2 -1
- package/dist/src/cli/commands/workspace-commands.js +3 -0
- package/dist/src/cli/program.js +9 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
- package/dist/src/services/config/config-types.d.ts +1 -1
- package/dist/src/services/context/artifact-meta.d.ts +72 -0
- package/dist/src/services/context/artifact-meta.js +105 -0
- package/dist/src/services/context/context-guard.d.ts +49 -0
- package/dist/src/services/context/context-guard.js +91 -0
- package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
- package/dist/src/services/context/dispatch-context-guard.js +192 -0
- package/dist/src/services/context/headroom-client.d.ts +34 -0
- package/dist/src/services/context/headroom-client.js +117 -0
- package/dist/src/services/context/shared-channel.d.ts +92 -0
- package/dist/src/services/context/shared-channel.js +285 -0
- package/dist/src/services/context/threshold.d.ts +35 -0
- package/dist/src/services/context/threshold.js +76 -0
- package/dist/src/services/dashboard/project-dashboard-service.d.ts +23 -0
- package/dist/src/services/dashboard/project-dashboard-service.js +21 -0
- package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
- package/dist/src/services/dispatch/batch-counter.js +85 -0
- package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
- package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
- package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
- package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
- package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
- package/dist/src/services/dispatch/leak-detector.js +72 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.js +80 -0
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +42 -0
- package/dist/src/services/ide/adapters/trae-adapter.js +98 -0
- package/dist/src/services/ide/hook-protocol.d.ts +47 -0
- package/dist/src/services/ide/hook-protocol.js +74 -0
- package/dist/src/services/ide/hook-translator.d.ts +72 -0
- package/dist/src/services/ide/hook-translator.js +128 -0
- package/dist/src/services/ide/ide-detector.d.ts +10 -0
- package/dist/src/services/ide/ide-detector.js +19 -0
- package/dist/src/services/ide/ide-registry.d.ts +14 -0
- package/dist/src/services/ide/ide-registry.js +45 -0
- package/dist/src/services/ide/ide-types.d.ts +180 -0
- package/dist/src/services/ide/ide-types.js +2 -0
- package/dist/src/services/ide/resource-profile.d.ts +52 -0
- package/dist/src/services/ide/resource-profile.js +33 -0
- package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
- package/dist/src/services/ide/shared/atomic-json.js +58 -0
- package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
- package/dist/src/services/ide/shared/safe-path.js +29 -0
- package/dist/src/services/memory/project-context-service.js +2 -1
- package/dist/src/services/memory/project-memory-service.js +4 -3
- package/dist/src/services/perf/perf-baseline-service.js +2 -1
- package/dist/src/services/progress/progress-service.d.ts +1 -1
- package/dist/src/services/progress/progress-service.js +18 -14
- package/dist/src/services/security/safe-settings-path.d.ts +12 -0
- package/dist/src/services/security/safe-settings-path.js +104 -0
- package/dist/src/services/session/getSessionDir.d.ts +1 -0
- package/dist/src/services/session/getSessionDir.js +27 -0
- package/dist/src/services/session/index.d.ts +1 -0
- package/dist/src/services/session/index.js +1 -0
- package/dist/src/services/signal/cancel-handler.d.ts +14 -0
- package/dist/src/services/signal/cancel-handler.js +76 -0
- package/dist/src/services/skill/resume-detector.d.ts +54 -0
- package/dist/src/services/skill/resume-detector.js +334 -0
- package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
- package/dist/src/services/skill/skill-scheduler.js +53 -0
- package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
- package/dist/src/services/skills/hooks-settings-service.js +190 -144
- package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
- package/dist/src/services/skills/statusline-settings-service.js +31 -34
- package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
- package/dist/src/services/slice/slice-archive-service.js +111 -0
- package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
- package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
- package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
- package/dist/src/services/solo/status-line-renderer.js +55 -0
- package/dist/src/services/standards/ide-aware-standards-service.d.ts +94 -0
- package/dist/src/services/standards/ide-aware-standards-service.js +89 -0
- package/dist/src/services/standards/project-standards-service.d.ts +1 -2
- package/dist/src/services/workspace/reconcile-service.d.ts +36 -0
- package/dist/src/services/workspace/reconcile-service.js +107 -6
- package/dist/src/services/workspace/reconcile-types.d.ts +12 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +2 -1
- package/scripts/install-skills.mjs +112 -2
- package/skills/peaks-ide/SKILL.md +159 -0
- package/skills/peaks-ide/references/audit-log-helper.md +52 -0
- package/skills/peaks-qa/SKILL.md +153 -55
- package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
- package/skills/peaks-rd/SKILL.md +134 -62
- package/skills/peaks-solo/SKILL.md +124 -37
- package/skills/peaks-solo/references/browser-workflow.md +22 -20
- package/skills/peaks-solo/references/context-governance.md +144 -0
- package/skills/peaks-solo/references/headroom-integration.md +107 -0
- package/skills/peaks-solo/references/runbook.md +3 -3
- package/skills/peaks-solo/references/sub-agent-dispatch.md +261 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
- package/skills/peaks-txt/SKILL.md +17 -0
- package/skills/peaks-ui/SKILL.md +45 -10
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slice #009 — SubAgentDispatcher abstraction.
|
|
3
|
+
*
|
|
4
|
+
* Per-IDE contract: given a sub-agent role + prompt + request/session ids,
|
|
5
|
+
* return a tool-call descriptor that the calling LLM should execute in
|
|
6
|
+
* its native environment. The CLI is IDE-agnostic; per-IDE tool names
|
|
7
|
+
* (Claude Code's `Task`, Trae's UNVERIFIED placeholder) are encapsulated
|
|
8
|
+
* here, never leaked to SKILL.md.
|
|
9
|
+
*
|
|
10
|
+
* Why this exists:
|
|
11
|
+
* - Prior SKILL.md hardcoded `Task(subagent_type="general-purpose", ...)`
|
|
12
|
+
* which made peaks-cli depend on Claude Code's specific sub-agent
|
|
13
|
+
* tool name. Adding a new IDE (Trae, future Cursor, etc.) required
|
|
14
|
+
* editing every SKILL.md that mentioned sub-agent dispatch.
|
|
15
|
+
* - This file (plus the per-IDE adapter wiring) collapses all
|
|
16
|
+
* per-IDE sub-agent specifics to a single `SubAgentDispatcher`
|
|
17
|
+
* instance per adapter. SKILL.md now only references
|
|
18
|
+
* `peaks sub-agent dispatch <role>`, and the IDE-private tool
|
|
19
|
+
* name flows through the returned `data.toolCall` at runtime.
|
|
20
|
+
*
|
|
21
|
+
* Cross-reference: PRD #002 G1 (AC-1..AC-5); RD tech-doc-002 §2.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Claude Code dispatcher. Real, byte-level implementation.
|
|
25
|
+
*
|
|
26
|
+
* - `supportsRole`: any non-empty string (Claude Code's
|
|
27
|
+
* `general-purpose` sub-agent accepts any prompt).
|
|
28
|
+
* - `buildToolCall`: returns `{name: 'Task', args: {subagent_type,
|
|
29
|
+
* description, prompt}}` — the exact shape the `Task` tool
|
|
30
|
+
* in Claude Code expects.
|
|
31
|
+
*/
|
|
32
|
+
export const claudeCodeSubAgentDispatcher = {
|
|
33
|
+
label: 'claude-code',
|
|
34
|
+
supportsRole: (role) => role.length > 0,
|
|
35
|
+
buildToolCall: ({ role, prompt, requestId }) => ({
|
|
36
|
+
name: 'Task',
|
|
37
|
+
args: {
|
|
38
|
+
subagent_type: 'general-purpose',
|
|
39
|
+
description: `${role} for rid=${requestId}`,
|
|
40
|
+
prompt,
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Trae dispatcher. UNVERIFIED — Trae sub-agent tool name TBD on real
|
|
46
|
+
* dogfood. Byte-level identical to claude-code by design so:
|
|
47
|
+
* 1. The slice #008 `subAgentToolMatcher: 'Task'` install entry
|
|
48
|
+
* stays byte-stable (the generated `.trae/settings.json` does
|
|
49
|
+
* not change).
|
|
50
|
+
* 2. The dispatcher's return shape is uniform across both
|
|
51
|
+
* adapters — a single byte-equality test can verify the
|
|
52
|
+
* placeholder contract.
|
|
53
|
+
*
|
|
54
|
+
* When real Trae dogfood lands, replace the body of `buildToolCall`
|
|
55
|
+
* with Trae's actual sub-agent tool name + args shape. The interface
|
|
56
|
+
* stays the same; only the per-IDE wiring breaks (intentionally).
|
|
57
|
+
*/
|
|
58
|
+
export const traeSubAgentDispatcher = {
|
|
59
|
+
// UNVERIFIED — see file header
|
|
60
|
+
label: 'trae',
|
|
61
|
+
supportsRole: (role) => role.length > 0,
|
|
62
|
+
buildToolCall: ({ role, prompt, requestId }) => ({
|
|
63
|
+
name: 'Task',
|
|
64
|
+
args: {
|
|
65
|
+
subagent_type: 'general-purpose',
|
|
66
|
+
description: `${role} for rid=${requestId}`,
|
|
67
|
+
prompt,
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Null dispatcher for IDEs that cannot dispatch sub-agents at all
|
|
73
|
+
* (e.g. a CLI-only IDE that has no LLM tool surface). Used as the
|
|
74
|
+
* fallback by future unsupported-IDE adapters. The CLI returns
|
|
75
|
+
* `{ok: false, code: "IDE_NOT_SUPPORTED"}` when the dispatcher's
|
|
76
|
+
* `supportsRole` returns false.
|
|
77
|
+
*/
|
|
78
|
+
export const nullSubAgentDispatcher = {
|
|
79
|
+
label: 'null',
|
|
80
|
+
supportsRole: () => false,
|
|
81
|
+
buildToolCall: ({ role }) => {
|
|
82
|
+
throw new SubAgentNotSupportedError(role);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Thrown by `nullSubAgentDispatcher.buildToolCall` and any future
|
|
87
|
+
* dispatcher that does not support a given role. The CLI catches
|
|
88
|
+
* this and returns the IDE_NOT_SUPPORTED error envelope.
|
|
89
|
+
*/
|
|
90
|
+
export class SubAgentNotSupportedError extends Error {
|
|
91
|
+
role;
|
|
92
|
+
code = 'IDE_NOT_SUPPORTED';
|
|
93
|
+
constructor(role) {
|
|
94
|
+
super(`Sub-agent dispatch is not supported for role: ${role}`);
|
|
95
|
+
this.role = role;
|
|
96
|
+
this.name = 'SubAgentNotSupportedError';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IdeAdapter } from '../ide-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code adapter —— peaks-cli 的"起源 IDE"。
|
|
4
|
+
*
|
|
5
|
+
* 该 adapter 从原 `src/services/skills/hooks-settings-service.ts` 提取,保持
|
|
6
|
+
* 字节级兼容:用户在 Claude Code 环境下跑 `peaks hooks install` 产出的
|
|
7
|
+
* `.claude/settings.json` 与 refactor 前逐字节相同。
|
|
8
|
+
*
|
|
9
|
+
* 字段解释(见 PRD AC-1):
|
|
10
|
+
* - dirName = '.claude' : Claude Code 项目根下的 settings 目录
|
|
11
|
+
* - settingsFileName = 'settings.json'
|
|
12
|
+
* - envVar = 'CLAUDE_PROJECT_DIR' : Claude Code 注入的 env 变量,用于 ${...} 占位
|
|
13
|
+
* - hookEvent = 'PreToolUse' : Claude Code hook 数组 key
|
|
14
|
+
* - toolMatcher = 'Bash' | 'Task' : PreToolUse 数组元素的 matcher 字段
|
|
15
|
+
*
|
|
16
|
+
* 不可消除的 per-IDE 字段(见 tech-doc.md §1.3)。
|
|
17
|
+
*/
|
|
18
|
+
export declare const CLAUDE_CODE_ADAPTER: IdeAdapter;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { claudeCodeSubAgentDispatcher } from '../../dispatch/sub-agent-dispatcher.js';
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code adapter —— peaks-cli 的"起源 IDE"。
|
|
6
|
+
*
|
|
7
|
+
* 该 adapter 从原 `src/services/skills/hooks-settings-service.ts` 提取,保持
|
|
8
|
+
* 字节级兼容:用户在 Claude Code 环境下跑 `peaks hooks install` 产出的
|
|
9
|
+
* `.claude/settings.json` 与 refactor 前逐字节相同。
|
|
10
|
+
*
|
|
11
|
+
* 字段解释(见 PRD AC-1):
|
|
12
|
+
* - dirName = '.claude' : Claude Code 项目根下的 settings 目录
|
|
13
|
+
* - settingsFileName = 'settings.json'
|
|
14
|
+
* - envVar = 'CLAUDE_PROJECT_DIR' : Claude Code 注入的 env 变量,用于 ${...} 占位
|
|
15
|
+
* - hookEvent = 'PreToolUse' : Claude Code hook 数组 key
|
|
16
|
+
* - toolMatcher = 'Bash' | 'Task' : PreToolUse 数组元素的 matcher 字段
|
|
17
|
+
*
|
|
18
|
+
* 不可消除的 per-IDE 字段(见 tech-doc.md §1.3)。
|
|
19
|
+
*/
|
|
20
|
+
export const CLAUDE_CODE_ADAPTER = {
|
|
21
|
+
id: 'claude-code',
|
|
22
|
+
displayName: 'Claude Code',
|
|
23
|
+
settings: {
|
|
24
|
+
dirName: '.claude',
|
|
25
|
+
settingsFileName: 'settings.json',
|
|
26
|
+
resolveSettingsFile: (scope, projectRoot) => {
|
|
27
|
+
const root = scope === 'global' ? homedir() : resolve(projectRoot ?? homedir());
|
|
28
|
+
return join(root, '.claude', 'settings.json');
|
|
29
|
+
},
|
|
30
|
+
supportsScope: () => true,
|
|
31
|
+
},
|
|
32
|
+
envVar: 'CLAUDE_PROJECT_DIR',
|
|
33
|
+
hookEvent: 'PreToolUse',
|
|
34
|
+
toolMatcher: 'Bash',
|
|
35
|
+
subAgentToolMatcher: 'Task',
|
|
36
|
+
// Slice #009: Claude Code uses the `Task` tool for sub-agent dispatch.
|
|
37
|
+
// The CLI calls `claudeCodeSubAgentDispatcher.buildToolCall` to construct
|
|
38
|
+
// the exact args shape the `Task` tool expects.
|
|
39
|
+
subAgentDispatcher: claudeCodeSubAgentDispatcher,
|
|
40
|
+
// Slice #010 G9: Claude Code supports the PreToolUse hook event in a
|
|
41
|
+
// form that can wrap `peaks sub-agent-dispatch-guard` as a sub-command.
|
|
42
|
+
// Opt in to the G9 hook install.
|
|
43
|
+
promptSizeAware: true,
|
|
44
|
+
installHints: [
|
|
45
|
+
'Restart Claude Code (or reload the window) so the PreToolUse hooks take effect.'
|
|
46
|
+
],
|
|
47
|
+
capabilities: {
|
|
48
|
+
gateEnforce: true,
|
|
49
|
+
progressStart: true,
|
|
50
|
+
statusline: true,
|
|
51
|
+
mcpInstall: true,
|
|
52
|
+
},
|
|
53
|
+
// Slice #011: standards profile. Claude Code reads its constitution at
|
|
54
|
+
// CLAUDE.md + module-level rules under .claude/rules/**. The values mirror
|
|
55
|
+
// the hardcoded paths in `src/services/standards/project-standards-service.ts`
|
|
56
|
+
// (line 147 = '.claude', line 417/421 = 'CLAUDE.md' + '.claude/rules/...')
|
|
57
|
+
// and the postinstall target in `scripts/install-skills.mjs` (line 427 =
|
|
58
|
+
// '~/.claude/skills'). Filling the profile here makes the dispatch layer
|
|
59
|
+
// route to the SAME paths, so byte-stability on `peaks standards init` for
|
|
60
|
+
// Claude Code projects is preserved.
|
|
61
|
+
standardsProfile: {
|
|
62
|
+
rootFile: 'CLAUDE.md',
|
|
63
|
+
rulesDir: '.claude/rules',
|
|
64
|
+
rulesFileGlob: '**/*.md',
|
|
65
|
+
autoLoaded: true,
|
|
66
|
+
format: 'markdown',
|
|
67
|
+
migrationHint: 'Standards live at CLAUDE.md + .claude/rules/** for Claude Code.',
|
|
68
|
+
},
|
|
69
|
+
// Slice #011: skill install profile. The postinstall script symlinks
|
|
70
|
+
// bundled skills to `~/.claude/skills` and writes output-styles to
|
|
71
|
+
// `~/.claude/output-styles`, matching the existing hardcoded
|
|
72
|
+
// install-skills.mjs lines 427 + 488. The env-var back-compat name
|
|
73
|
+
// matches the legacy `PEAKS_CLAUDE_SKILLS_DIR` / `PEAKS_CLAUDE_OUTPUT_STYLES_DIR`.
|
|
74
|
+
skillInstall: {
|
|
75
|
+
skillsDir: join(homedir(), '.claude', 'skills'),
|
|
76
|
+
outputStylesDir: join(homedir(), '.claude', 'output-styles'),
|
|
77
|
+
installStrategy: 'symlink',
|
|
78
|
+
envVarOverride: 'PEAKS_CLAUDE_SKILLS_DIR',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { IdeAdapter } from '../ide-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Trae IDE adapter —— peaks-cli 的第二个内置 IDE 适配器。
|
|
4
|
+
*
|
|
5
|
+
* 不可消除的 per-IDE 字段(slice #1 锁定):
|
|
6
|
+
* - settings.dirName = '.trae' : Trae 项目根下的配置目录
|
|
7
|
+
* - settings.settingsFileName = 'settings.json' (VERIFIED against Trae 1.x fixture, slice 009-009-2026-06-07-trae-dogfood)
|
|
8
|
+
* - envVar = 'TRAE_PROJECT_DIR' : Trae 注入的 env 变量(用于 ${...} 占位)
|
|
9
|
+
* - hookEvent = 'beforeToolCall' (VERIFIED against Trae 1.x fixture, slice 009-009-2026-06-07-trae-dogfood)
|
|
10
|
+
* - toolMatcher = 'terminal' (VERIFIED against Trae 1.x fixture, slice 009-009-2026-06-07-trae-dogfood)
|
|
11
|
+
*
|
|
12
|
+
* Slice #1 的 slim `IdeAdapter` shape 在 slice #1 RD 中被锁为"填表"模式。
|
|
13
|
+
* 本文件是 slice #2 第一个真实客户,验证 slice #1 抽出的形状真的可以
|
|
14
|
+
* 简单复制粘贴就接入新 IDE。
|
|
15
|
+
*
|
|
16
|
+
* 与 slice #1 claude-code-adapter.ts 的区别(故意):
|
|
17
|
+
* - Trae 的 hookEvent 名是 `beforeToolCall` 而不是 `PreToolUse`(VERIFIED)
|
|
18
|
+
* - Trae 的 toolMatcher 是 `terminal` 而不是 `Bash`(VERIFIED)
|
|
19
|
+
* - Trae 的 settings 路径是 `.trae/settings.json`(同 Claude 风格,只是目录名不同;VERIFIED)
|
|
20
|
+
* - Trae 的 envVar 是 `TRAE_PROJECT_DIR`
|
|
21
|
+
* - installHints 提示用户"重启 Trae"(同 Claude 风格)
|
|
22
|
+
*
|
|
23
|
+
* Slice #009 验证结论(2026-06-07):
|
|
24
|
+
* - 4 UNVERIFIED fields are all VERIFIED-AS-IS against the Trae 1.x fixture
|
|
25
|
+
* (tests/fixtures/trae/trae-1x-payload.json) AND the live install
|
|
26
|
+
* dispatch path exercised by `peaks hooks install` / `peaks statusline
|
|
27
|
+
* install` / `peaks hook handle`. The fixture mimics a real Trae 1.x
|
|
28
|
+
* install's payload shape; the dispatch path is the byte-level same path
|
|
29
|
+
* a real Trae install would trigger. Caveat: a follow-up slice should
|
|
30
|
+
* re-run the same 5+ dogfood paths on a real Trae 1.x install once one
|
|
31
|
+
* is available, to confirm the 1.x assumption is correct (see PRD R-1
|
|
32
|
+
* + the new memory at
|
|
33
|
+
* .peaks/memory/trae-adapter-values-verified-against-1x.md).
|
|
34
|
+
* - See .peaks/_runtime/2026-06-06-session-5b1095/qa/dogfood-trae-1x-2026-06-07.md
|
|
35
|
+
* for the full resolution table.
|
|
36
|
+
*
|
|
37
|
+
* Slice #3 refactor: the `peaks hooks install` command now dispatches on the
|
|
38
|
+
* IDE adapter (auto-detect from env / cwd, override with `--ide trae`). When
|
|
39
|
+
* a Trae install is run, the resulting `<root>/.trae/settings.json` will use
|
|
40
|
+
* the `beforeToolCall` event key and the `terminal` matcher from this adapter.
|
|
41
|
+
*/
|
|
42
|
+
export declare const TRAE_ADAPTER: IdeAdapter;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { traeSubAgentDispatcher } from '../../dispatch/sub-agent-dispatcher.js';
|
|
4
|
+
/**
|
|
5
|
+
* Trae IDE adapter —— peaks-cli 的第二个内置 IDE 适配器。
|
|
6
|
+
*
|
|
7
|
+
* 不可消除的 per-IDE 字段(slice #1 锁定):
|
|
8
|
+
* - settings.dirName = '.trae' : Trae 项目根下的配置目录
|
|
9
|
+
* - settings.settingsFileName = 'settings.json' (VERIFIED against Trae 1.x fixture, slice 009-009-2026-06-07-trae-dogfood)
|
|
10
|
+
* - envVar = 'TRAE_PROJECT_DIR' : Trae 注入的 env 变量(用于 ${...} 占位)
|
|
11
|
+
* - hookEvent = 'beforeToolCall' (VERIFIED against Trae 1.x fixture, slice 009-009-2026-06-07-trae-dogfood)
|
|
12
|
+
* - toolMatcher = 'terminal' (VERIFIED against Trae 1.x fixture, slice 009-009-2026-06-07-trae-dogfood)
|
|
13
|
+
*
|
|
14
|
+
* Slice #1 的 slim `IdeAdapter` shape 在 slice #1 RD 中被锁为"填表"模式。
|
|
15
|
+
* 本文件是 slice #2 第一个真实客户,验证 slice #1 抽出的形状真的可以
|
|
16
|
+
* 简单复制粘贴就接入新 IDE。
|
|
17
|
+
*
|
|
18
|
+
* 与 slice #1 claude-code-adapter.ts 的区别(故意):
|
|
19
|
+
* - Trae 的 hookEvent 名是 `beforeToolCall` 而不是 `PreToolUse`(VERIFIED)
|
|
20
|
+
* - Trae 的 toolMatcher 是 `terminal` 而不是 `Bash`(VERIFIED)
|
|
21
|
+
* - Trae 的 settings 路径是 `.trae/settings.json`(同 Claude 风格,只是目录名不同;VERIFIED)
|
|
22
|
+
* - Trae 的 envVar 是 `TRAE_PROJECT_DIR`
|
|
23
|
+
* - installHints 提示用户"重启 Trae"(同 Claude 风格)
|
|
24
|
+
*
|
|
25
|
+
* Slice #009 验证结论(2026-06-07):
|
|
26
|
+
* - 4 UNVERIFIED fields are all VERIFIED-AS-IS against the Trae 1.x fixture
|
|
27
|
+
* (tests/fixtures/trae/trae-1x-payload.json) AND the live install
|
|
28
|
+
* dispatch path exercised by `peaks hooks install` / `peaks statusline
|
|
29
|
+
* install` / `peaks hook handle`. The fixture mimics a real Trae 1.x
|
|
30
|
+
* install's payload shape; the dispatch path is the byte-level same path
|
|
31
|
+
* a real Trae install would trigger. Caveat: a follow-up slice should
|
|
32
|
+
* re-run the same 5+ dogfood paths on a real Trae 1.x install once one
|
|
33
|
+
* is available, to confirm the 1.x assumption is correct (see PRD R-1
|
|
34
|
+
* + the new memory at
|
|
35
|
+
* .peaks/memory/trae-adapter-values-verified-against-1x.md).
|
|
36
|
+
* - See .peaks/_runtime/2026-06-06-session-5b1095/qa/dogfood-trae-1x-2026-06-07.md
|
|
37
|
+
* for the full resolution table.
|
|
38
|
+
*
|
|
39
|
+
* Slice #3 refactor: the `peaks hooks install` command now dispatches on the
|
|
40
|
+
* IDE adapter (auto-detect from env / cwd, override with `--ide trae`). When
|
|
41
|
+
* a Trae install is run, the resulting `<root>/.trae/settings.json` will use
|
|
42
|
+
* the `beforeToolCall` event key and the `terminal` matcher from this adapter.
|
|
43
|
+
*/
|
|
44
|
+
export const TRAE_ADAPTER = {
|
|
45
|
+
id: 'trae',
|
|
46
|
+
displayName: 'Trae',
|
|
47
|
+
settings: {
|
|
48
|
+
dirName: '.trae',
|
|
49
|
+
settingsFileName: 'settings.json', // VERIFIED against Trae 1.x fixture — slice 009-009-2026-06-07-trae-dogfood (2026-06-07)
|
|
50
|
+
resolveSettingsFile: (scope, projectRoot) => {
|
|
51
|
+
const root = scope === 'global' ? homedir() : resolve(projectRoot ?? homedir());
|
|
52
|
+
return join(root, '.trae', 'settings.json');
|
|
53
|
+
},
|
|
54
|
+
supportsScope: (scope) => scope === 'project' || scope === 'global'
|
|
55
|
+
},
|
|
56
|
+
envVar: 'TRAE_PROJECT_DIR',
|
|
57
|
+
hookEvent: 'beforeToolCall', // VERIFIED against Trae 1.x fixture — slice 009-009-2026-06-07-trae-dogfood (2026-06-07); fixture at tests/fixtures/trae/trae-1x-payload.json
|
|
58
|
+
toolMatcher: 'terminal', // VERIFIED against Trae 1.x fixture — slice 009-009-2026-06-07-trae-dogfood (2026-06-07); fixture pins `parameters.tool: 'terminal'`
|
|
59
|
+
subAgentToolMatcher: 'Task', // UNVERIFIED — Trae's sub-agent tool name is unknown; matches the prior hardcoded 'Task' literal so byte-level install output is unchanged. Will be dogfooded when a real Trae 1.x install dispatches a sub-agent.
|
|
60
|
+
// Slice #009: Trae's sub-agent dispatcher is UNVERIFIED — Trae sub-agent
|
|
61
|
+
// tool name TBD on real dogfood; byte-level identical to claude-code by
|
|
62
|
+
// design so the slice #008 `subAgentToolMatcher: 'Task'` install entry
|
|
63
|
+
// stays byte-stable. Awaiting real Trae 1.x dogfood to confirm/replace.
|
|
64
|
+
subAgentDispatcher: traeSubAgentDispatcher,
|
|
65
|
+
// Slice #010 G9: Trae supports `beforeToolCall` which can wrap
|
|
66
|
+
// `peaks sub-agent-dispatch-guard`. Opt in (matches the byte-stable
|
|
67
|
+
// slice #008 install entry shape).
|
|
68
|
+
promptSizeAware: true,
|
|
69
|
+
installHints: [
|
|
70
|
+
'Restart Trae (or reload the workspace) so the beforeToolCall hooks take effect.'
|
|
71
|
+
],
|
|
72
|
+
capabilities: {
|
|
73
|
+
gateEnforce: true,
|
|
74
|
+
progressStart: true,
|
|
75
|
+
statusline: true,
|
|
76
|
+
// Slice #007-007-2026-06-07-mcp-decouple: mcpInstall is LOAD-BEARING.
|
|
77
|
+
// The 4 MCP capabilities (playwright, chrome-devtools, figma, context7)
|
|
78
|
+
// are installed via `peaks mcp plan/apply` which writes to the global
|
|
79
|
+
// `~/.claude/settings.json` file. Trae 1.x's MCP integration is
|
|
80
|
+
// UNVERIFIED (the Trae fixture did not dogfood the MCP install path),
|
|
81
|
+
// so the 6 SKILL.md files must surface a Trae-specific path (manual
|
|
82
|
+
// install + manual tool invocation) rather than promising `peaks mcp
|
|
83
|
+
// apply` will work on Trae. Skill bodies consume this flag through
|
|
84
|
+
// the IDE adapter's `capabilities.mcpInstall`; setting it to true
|
|
85
|
+
// without a real Trae MCP install dogfood would be a regression.
|
|
86
|
+
// Cross-reference: .peaks/memory/trae-adapter-sets-mcpinstall-false-trae-mcp-integration-is-unverified.md
|
|
87
|
+
// and the 4 per-capability memos under .peaks/memory/mcp-decouple-*.md.
|
|
88
|
+
mcpInstall: false
|
|
89
|
+
}
|
|
90
|
+
// Standards: UNVERIFIED — see slice #012+ (Trae real-install dogfood for
|
|
91
|
+
// the `standardsProfile` and `skillInstall` fields). The slice #011
|
|
92
|
+
// framework lands; per-IDE values for Trae are a follow-up gated on
|
|
93
|
+
// the user's real Trae 1.x install. Until then, `peaks standards init`
|
|
94
|
+
// on a Trae-detected project falls back to the Claude Code path
|
|
95
|
+
// (CLAUDE.md + .claude/rules/**) with a stderr warning, and the
|
|
96
|
+
// postinstall script writes skills + output-styles to the legacy
|
|
97
|
+
// `~/.claude/{skills,output-styles}` paths with a stderr warning.
|
|
98
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PEAKS_HOOK_SCHEMA, type IdeId, type PeaksCanonicalHook, type PeaksDecisionTransport } from './ide-types.js';
|
|
2
|
+
export { PEAKS_HOOK_SCHEMA };
|
|
3
|
+
export type { PeaksCanonicalHook, PeaksDecisionTransport };
|
|
4
|
+
/**
|
|
5
|
+
* Compute the deny decision shape for Claude Code (the only adapter registered
|
|
6
|
+
* in slice #1). The output is a JSON object that, when written to stdout, makes
|
|
7
|
+
* the Claude Code permission system block the tool call BEFORE the user's
|
|
8
|
+
* permission prompt — un-bypassable, even under --dangerously-skip-permissions.
|
|
9
|
+
*/
|
|
10
|
+
export declare const CLAUDE_CODE_DENY_SHAPE: Record<string, unknown>;
|
|
11
|
+
export declare const CLAUDE_CODE_DENY_TRANSPORT: PeaksDecisionTransport;
|
|
12
|
+
/**
|
|
13
|
+
* Compute the deny decision shape for Trae (Cursor-style sibling IDE).
|
|
14
|
+
* VERIFIED against Trae 1.x fixture — slice 009-009-2026-06-07-trae-dogfood (2026-06-07).
|
|
15
|
+
* Shape is the standard Cursor/Claude sibling envelope: `hookSpecificOutput`
|
|
16
|
+
* with `hookEventName`, `permissionDecision`, `permissionDecisionReason`.
|
|
17
|
+
* The hookEventName is `'beforeToolCall'` for Trae vs `'PreToolUse'` for
|
|
18
|
+
* Claude Code — the only field that differs between the two siblings.
|
|
19
|
+
* Fixture: tests/fixtures/trae/trae-1x-payload.json. Constant NAME preserved
|
|
20
|
+
* (per slice 009 PRD R-3); only the shape was already correct from slice #3.
|
|
21
|
+
*/
|
|
22
|
+
export declare const TRAE_DENY_SHAPE: Record<string, unknown>;
|
|
23
|
+
export declare const TRAE_DENY_TRANSPORT: PeaksDecisionTransport;
|
|
24
|
+
/**
|
|
25
|
+
* Format a decision response for a given IDE. Slice #1 handles Claude Code;
|
|
26
|
+
* slice #3 added Trae (1.x-assumption shape — see TRAE_DENY_SHAPE doc).
|
|
27
|
+
* Future slices will add exit-code / both variants for IDEs that don't read
|
|
28
|
+
* stdout.
|
|
29
|
+
*/
|
|
30
|
+
export declare function formatDecisionResponse(ide: IdeId, decision: 'allow' | 'deny', reason?: string): {
|
|
31
|
+
stdout: string;
|
|
32
|
+
exitCode: number;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Build a peaks canonical hook from a parsed stdin payload. Caller has already
|
|
36
|
+
* done stdin parsing + IDE auto-detection; this function normalizes to the
|
|
37
|
+
* canonical schema.
|
|
38
|
+
*/
|
|
39
|
+
export interface BuildCanonicalHookInput {
|
|
40
|
+
readonly toolName: string;
|
|
41
|
+
readonly toolInput: Record<string, unknown>;
|
|
42
|
+
readonly projectRoot: string;
|
|
43
|
+
readonly rawIdeFormat: IdeId;
|
|
44
|
+
readonly rawPayload: unknown;
|
|
45
|
+
readonly event?: PeaksCanonicalHook['event'];
|
|
46
|
+
}
|
|
47
|
+
export declare function buildCanonicalHook(input: BuildCanonicalHookInput): PeaksCanonicalHook;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { PEAKS_HOOK_SCHEMA } from './ide-types.js';
|
|
2
|
+
export { PEAKS_HOOK_SCHEMA };
|
|
3
|
+
/**
|
|
4
|
+
* Compute the deny decision shape for Claude Code (the only adapter registered
|
|
5
|
+
* in slice #1). The output is a JSON object that, when written to stdout, makes
|
|
6
|
+
* the Claude Code permission system block the tool call BEFORE the user's
|
|
7
|
+
* permission prompt — un-bypassable, even under --dangerously-skip-permissions.
|
|
8
|
+
*/
|
|
9
|
+
export const CLAUDE_CODE_DENY_SHAPE = {
|
|
10
|
+
hookSpecificOutput: {
|
|
11
|
+
hookEventName: 'PreToolUse',
|
|
12
|
+
permissionDecision: 'deny',
|
|
13
|
+
permissionDecisionReason: '__REASON__' // replaced at format time
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
export const CLAUDE_CODE_DENY_TRANSPORT = {
|
|
17
|
+
kind: 'stdout-json',
|
|
18
|
+
denyShape: CLAUDE_CODE_DENY_SHAPE
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Compute the deny decision shape for Trae (Cursor-style sibling IDE).
|
|
22
|
+
* VERIFIED against Trae 1.x fixture — slice 009-009-2026-06-07-trae-dogfood (2026-06-07).
|
|
23
|
+
* Shape is the standard Cursor/Claude sibling envelope: `hookSpecificOutput`
|
|
24
|
+
* with `hookEventName`, `permissionDecision`, `permissionDecisionReason`.
|
|
25
|
+
* The hookEventName is `'beforeToolCall'` for Trae vs `'PreToolUse'` for
|
|
26
|
+
* Claude Code — the only field that differs between the two siblings.
|
|
27
|
+
* Fixture: tests/fixtures/trae/trae-1x-payload.json. Constant NAME preserved
|
|
28
|
+
* (per slice 009 PRD R-3); only the shape was already correct from slice #3.
|
|
29
|
+
*/
|
|
30
|
+
export const TRAE_DENY_SHAPE = {
|
|
31
|
+
hookSpecificOutput: {
|
|
32
|
+
hookEventName: 'beforeToolCall',
|
|
33
|
+
permissionDecision: 'deny',
|
|
34
|
+
permissionDecisionReason: '__REASON__' // replaced at format time
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
export const TRAE_DENY_TRANSPORT = {
|
|
38
|
+
kind: 'stdout-json',
|
|
39
|
+
denyShape: TRAE_DENY_SHAPE
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Format a decision response for a given IDE. Slice #1 handles Claude Code;
|
|
43
|
+
* slice #3 added Trae (1.x-assumption shape — see TRAE_DENY_SHAPE doc).
|
|
44
|
+
* Future slices will add exit-code / both variants for IDEs that don't read
|
|
45
|
+
* stdout.
|
|
46
|
+
*/
|
|
47
|
+
export function formatDecisionResponse(ide, decision, reason) {
|
|
48
|
+
if (decision === 'allow') {
|
|
49
|
+
return { stdout: '', exitCode: 0 };
|
|
50
|
+
}
|
|
51
|
+
let shape;
|
|
52
|
+
if (ide === 'claude-code') {
|
|
53
|
+
shape = CLAUDE_CODE_DENY_SHAPE;
|
|
54
|
+
}
|
|
55
|
+
else if (ide === 'trae') {
|
|
56
|
+
shape = TRAE_DENY_SHAPE;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
throw new Error(`formatDecisionResponse: unsupported IDE ${ide} (not registered in adapter registry; future slice will add support)`);
|
|
60
|
+
}
|
|
61
|
+
const filled = JSON.stringify(shape).replace('"__REASON__"', JSON.stringify(reason ?? 'denied'));
|
|
62
|
+
return { stdout: filled, exitCode: 0 };
|
|
63
|
+
}
|
|
64
|
+
export function buildCanonicalHook(input) {
|
|
65
|
+
return {
|
|
66
|
+
schema: PEAKS_HOOK_SCHEMA,
|
|
67
|
+
event: input.event ?? 'pre-tool-use',
|
|
68
|
+
toolName: input.toolName,
|
|
69
|
+
toolInput: input.toolInput,
|
|
70
|
+
projectRoot: input.projectRoot,
|
|
71
|
+
rawIdeFormat: input.rawIdeFormat,
|
|
72
|
+
rawPayload: input.rawPayload
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { IdeId } from './ide-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* hook-translator —— peaks 自有 hook 协议的核心。
|
|
4
|
+
*
|
|
5
|
+
* 单一职责:把 IDE 私有 stdin 形态归一化到 peaks canonical schema;把 peaks 决策
|
|
6
|
+
* 格式化回 IDE 期望的 stdout/exit-code 形态。
|
|
7
|
+
*
|
|
8
|
+
* auto-detection 算法(优先级从高到低):
|
|
9
|
+
* 1. env 变量:CLAUDE_PROJECT_DIR / TRAE_PROJECT_DIR / CODEX_PROJECT_DIR / ...
|
|
10
|
+
* 2. stdin shape:`{ tool_name, tool_input }` 是 Claude / Trae;
|
|
11
|
+
* `{ toolName, toolInput }` 是 Cursor;
|
|
12
|
+
* `{ eventName, parameters }` 是 Trae 另形态
|
|
13
|
+
* 3. cwd 启发式:存在 .claude / .trae / .codex / .cursor 目录
|
|
14
|
+
* 4. fallback:`claude-code`(backward compat,见 PRD preserved behavior #10)
|
|
15
|
+
*/
|
|
16
|
+
export interface DetectFromStdinInput {
|
|
17
|
+
readonly env: NodeJS.ProcessEnv;
|
|
18
|
+
readonly cwd: string;
|
|
19
|
+
/** 已解析的 stdin 形态;null = 空 stdin 或非 JSON */
|
|
20
|
+
readonly parsedStdin: unknown;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Detect the originating IDE from env / stdin shape / cwd heuristics.
|
|
24
|
+
* Falls back to 'claude-code' (PRD preserved behavior: backward compat).
|
|
25
|
+
*/
|
|
26
|
+
export declare function detectIdeFromContext(input: DetectFromStdinInput): IdeId;
|
|
27
|
+
/**
|
|
28
|
+
* Pluck a string value at a nested path. Returns undefined if any segment is
|
|
29
|
+
* missing or non-object. Used by adapter-driven stdin parsers.
|
|
30
|
+
*/
|
|
31
|
+
export declare function pluckString(obj: unknown, path: readonly string[]): string | undefined;
|
|
32
|
+
export declare function pluckObject(obj: unknown, path: readonly string[]): Record<string, unknown> | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Default stdin parser for adapters that follow the Claude Code shape
|
|
35
|
+
* (most LLM-style IDEs do: tool_name at top, tool_input.{command} inside).
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseClaudeShapeStdin(parsed: unknown): {
|
|
38
|
+
toolName?: string;
|
|
39
|
+
command?: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Trae stdin parser. UNVERIFIED — Trae 1.x's actual stdin envelope shape is
|
|
43
|
+
* assumed to be Cursor-style (eventName at top, parameters nested), based on
|
|
44
|
+
* slice #2's read of Trae as a "Cursor sibling". When a real Trae 1.x hook
|
|
45
|
+
* payload is dogfooded, this parser is the seam to update.
|
|
46
|
+
*
|
|
47
|
+
* Shape (assumed):
|
|
48
|
+
* { eventName: 'beforeToolCall', parameters: { command: 'rm -rf /' } }
|
|
49
|
+
*
|
|
50
|
+
* Returns the same `{ toolName, command }` shape as parseClaudeShapeStdin so
|
|
51
|
+
* downstream code (buildCanonicalHook, enforceBashCommand) does not need to
|
|
52
|
+
* branch on IDE. The `toolName` here is the IDE-specific name (e.g.
|
|
53
|
+
* 'terminal'); callers may normalize it to the canonical 'Bash' if needed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseTraeShapeStdin(parsed: unknown): {
|
|
56
|
+
toolName?: string;
|
|
57
|
+
command?: string;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Per-adapter stdin parser dispatch. Returns the `{ toolName, command }` pair
|
|
61
|
+
* for the given IDE, regardless of which stdin shape the IDE actually emits.
|
|
62
|
+
*
|
|
63
|
+
* The dispatch table is keyed on `IdeId` (not stdin shape) so an adapter can
|
|
64
|
+
* change its wire format without breaking the hook-handle flow. Unknown IDEs
|
|
65
|
+
* fall back to the Claude parser — that preserves the slice #1 "fail-open to
|
|
66
|
+
* Claude" behavior so a future adapter that has not yet been added does not
|
|
67
|
+
* crash the hook runtime.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseAdapterStdin(ide: IdeId, parsed: unknown): {
|
|
70
|
+
toolName?: string;
|
|
71
|
+
command?: string;
|
|
72
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { getAdapter, listAdapterIds } from './ide-registry.js';
|
|
4
|
+
/**
|
|
5
|
+
* Detect the originating IDE from env / stdin shape / cwd heuristics.
|
|
6
|
+
* Falls back to 'claude-code' (PRD preserved behavior: backward compat).
|
|
7
|
+
*/
|
|
8
|
+
export function detectIdeFromContext(input) {
|
|
9
|
+
// 1. env 变量优先级最高。Iterate ONLY over currently-registered adapters
|
|
10
|
+
// so the function does not throw on unregistered IDEs while future slices
|
|
11
|
+
// progressively add trae / codex / cursor / qoder / tongyi-lingma.
|
|
12
|
+
for (const adapter of listAdapterIds()) {
|
|
13
|
+
const a = getAdapter(adapter);
|
|
14
|
+
if (input.env[a.envVar] !== undefined) {
|
|
15
|
+
return adapter;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// 2. stdin shape
|
|
19
|
+
if (isObject(input.parsedStdin)) {
|
|
20
|
+
if ('tool_name' in input.parsedStdin || 'tool_input' in input.parsedStdin) {
|
|
21
|
+
return 'claude-code';
|
|
22
|
+
}
|
|
23
|
+
if ('toolName' in input.parsedStdin || 'toolInput' in input.parsedStdin) {
|
|
24
|
+
return 'cursor';
|
|
25
|
+
}
|
|
26
|
+
if ('eventName' in input.parsedStdin || 'parameters' in input.parsedStdin) {
|
|
27
|
+
return 'trae';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// 3. cwd 启发式。Same registration-aware iteration as step 1.
|
|
31
|
+
for (const adapter of listAdapterIds()) {
|
|
32
|
+
const a = getAdapter(adapter);
|
|
33
|
+
if (existsSync(join(input.cwd, a.settings.dirName))) {
|
|
34
|
+
return adapter;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// 4. fallback
|
|
38
|
+
return 'claude-code';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Pluck a string value at a nested path. Returns undefined if any segment is
|
|
42
|
+
* missing or non-object. Used by adapter-driven stdin parsers.
|
|
43
|
+
*/
|
|
44
|
+
export function pluckString(obj, path) {
|
|
45
|
+
let cur = obj;
|
|
46
|
+
for (const seg of path) {
|
|
47
|
+
if (!isObject(cur) || !(seg in cur))
|
|
48
|
+
return undefined;
|
|
49
|
+
cur = cur[seg];
|
|
50
|
+
}
|
|
51
|
+
return typeof cur === 'string' ? cur : undefined;
|
|
52
|
+
}
|
|
53
|
+
export function pluckObject(obj, path) {
|
|
54
|
+
let cur = obj;
|
|
55
|
+
for (const seg of path) {
|
|
56
|
+
if (!isObject(cur) || !(seg in cur))
|
|
57
|
+
return undefined;
|
|
58
|
+
cur = cur[seg];
|
|
59
|
+
}
|
|
60
|
+
return isObject(cur) ? cur : undefined;
|
|
61
|
+
}
|
|
62
|
+
function isObject(v) {
|
|
63
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Default stdin parser for adapters that follow the Claude Code shape
|
|
67
|
+
* (most LLM-style IDEs do: tool_name at top, tool_input.{command} inside).
|
|
68
|
+
*/
|
|
69
|
+
export function parseClaudeShapeStdin(parsed) {
|
|
70
|
+
if (!isObject(parsed))
|
|
71
|
+
return {};
|
|
72
|
+
const toolName = pluckString(parsed, ['tool_name']);
|
|
73
|
+
const command = pluckString(parsed, ['tool_input', 'command']);
|
|
74
|
+
const result = {};
|
|
75
|
+
if (toolName !== undefined)
|
|
76
|
+
result.toolName = toolName;
|
|
77
|
+
if (command !== undefined)
|
|
78
|
+
result.command = command;
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Trae stdin parser. UNVERIFIED — Trae 1.x's actual stdin envelope shape is
|
|
83
|
+
* assumed to be Cursor-style (eventName at top, parameters nested), based on
|
|
84
|
+
* slice #2's read of Trae as a "Cursor sibling". When a real Trae 1.x hook
|
|
85
|
+
* payload is dogfooded, this parser is the seam to update.
|
|
86
|
+
*
|
|
87
|
+
* Shape (assumed):
|
|
88
|
+
* { eventName: 'beforeToolCall', parameters: { command: 'rm -rf /' } }
|
|
89
|
+
*
|
|
90
|
+
* Returns the same `{ toolName, command }` shape as parseClaudeShapeStdin so
|
|
91
|
+
* downstream code (buildCanonicalHook, enforceBashCommand) does not need to
|
|
92
|
+
* branch on IDE. The `toolName` here is the IDE-specific name (e.g.
|
|
93
|
+
* 'terminal'); callers may normalize it to the canonical 'Bash' if needed.
|
|
94
|
+
*/
|
|
95
|
+
export function parseTraeShapeStdin(parsed) {
|
|
96
|
+
if (!isObject(parsed))
|
|
97
|
+
return {};
|
|
98
|
+
const eventName = pluckString(parsed, ['eventName']);
|
|
99
|
+
const command = pluckString(parsed, ['parameters', 'command']);
|
|
100
|
+
const result = {};
|
|
101
|
+
// Trae sends the event name in the payload (`eventName: 'beforeToolCall'`)
|
|
102
|
+
// and the tool name on the parameters (e.g. `parameters.tool: 'terminal'`).
|
|
103
|
+
// We expose the event name as the "toolName" for now since Trae has not
|
|
104
|
+
// been observed to carry a top-level tool field. Future slice can split
|
|
105
|
+
// event vs tool if the real shape requires it.
|
|
106
|
+
if (eventName !== undefined)
|
|
107
|
+
result.toolName = eventName;
|
|
108
|
+
if (command !== undefined)
|
|
109
|
+
result.command = command;
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Per-adapter stdin parser dispatch. Returns the `{ toolName, command }` pair
|
|
114
|
+
* for the given IDE, regardless of which stdin shape the IDE actually emits.
|
|
115
|
+
*
|
|
116
|
+
* The dispatch table is keyed on `IdeId` (not stdin shape) so an adapter can
|
|
117
|
+
* change its wire format without breaking the hook-handle flow. Unknown IDEs
|
|
118
|
+
* fall back to the Claude parser — that preserves the slice #1 "fail-open to
|
|
119
|
+
* Claude" behavior so a future adapter that has not yet been added does not
|
|
120
|
+
* crash the hook runtime.
|
|
121
|
+
*/
|
|
122
|
+
export function parseAdapterStdin(ide, parsed) {
|
|
123
|
+
if (ide === 'trae')
|
|
124
|
+
return parseTraeShapeStdin(parsed);
|
|
125
|
+
// Future adapters (codex, cursor, qoder, tongyi-lingma) — branch here when
|
|
126
|
+
// their adapters are registered. Until then, fall back to Claude shape.
|
|
127
|
+
return parseClaudeShapeStdin(parsed);
|
|
128
|
+
}
|