@logicseek/hello-cc 1.0.1 → 1.0.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/CHANGELOG.md +105 -0
- package/README.md +4 -0
- package/README.zh-CN.md +4 -0
- package/docs/README.md +9 -0
- package/docs/README.zh-CN.md +9 -0
- package/docs/commands.md +53 -0
- package/docs/commands.zh-CN.md +48 -0
- package/docs/dsh.md +137 -0
- package/docs/dsh.zh-CN.md +139 -0
- package/docs/mcp.md +9 -0
- package/docs/mcp.zh-CN.md +23 -0
- package/docs/native.md +295 -0
- package/docs/native.zh-CN.md +253 -0
- package/docs/web-handoff.zh-CN.md +53 -0
- package/lib/cli/app.mjs +25 -6
- package/lib/cli/commands/doctor.mjs +17 -2
- package/lib/cli/commands/dsh.mjs +79 -0
- package/lib/cli/commands/hook.mjs +241 -90
- package/lib/cli/commands/lock.mjs +34 -19
- package/lib/cli/commands/mcp.mjs +28 -0
- package/lib/cli/commands/native.mjs +130 -0
- package/lib/cli/commands/run.mjs +66 -21
- package/lib/cli/commands/task.mjs +6 -3
- package/lib/cli-args.mjs +4 -0
- package/lib/coordination-state.mjs +15 -1
- package/lib/core/coordination/event-retention.mjs +14 -2
- package/lib/core/coordination/gc-plan.mjs +3 -13
- package/lib/core/coordination/locks.mjs +45 -0
- package/lib/core/coordination/results.mjs +79 -0
- package/lib/core/peers/bindings.mjs +9 -3
- package/lib/diagnostics/codex.mjs +121 -0
- package/lib/integrations/codex-interactions.mjs +114 -0
- package/lib/integrations/dsh-collaboration.mjs +170 -0
- package/lib/integrations/dsh-cordis.d.ts +19 -0
- package/lib/integrations/dsh-cordis.mjs +177 -0
- package/lib/integrations/dsh.bundle.yml +7 -0
- package/lib/integrations/dsh.mjs +287 -0
- package/lib/integrations/native/claude.mjs +451 -0
- package/lib/integrations/native/codex.mjs +242 -0
- package/lib/integrations/native/dsh-acp.mjs +343 -0
- package/lib/integrations/native/index.mjs +19 -0
- package/lib/integrations/native/interactions.mjs +53 -0
- package/lib/integrations/native/jsonrpc.mjs +185 -0
- package/lib/integrations/peers/identity.mjs +36 -10
- package/lib/integrations/peers/processes.mjs +22 -8
- package/lib/integrations/providers.mjs +6 -2
- package/lib/mcp/scope.mjs +104 -0
- package/lib/mcp/stdio.mjs +80 -0
- package/lib/mcp/tools.mjs +197 -0
- package/lib/process/identity.mjs +3 -1
- package/lib/release/release-notes.mjs +9 -0
- package/lib/runtime/native/client.mjs +38 -0
- package/lib/runtime/native/service.mjs +511 -0
- package/lib/runtime/native/store.mjs +121 -0
- package/lib/ui/help.mjs +5 -2
- package/lib/web/adoption-state.mjs +55 -0
- package/lib/web/auto-attach.mjs +18 -2
- package/lib/web/codex-app-server.mjs +557 -0
- package/lib/web/codex-sessions.mjs +287 -0
- package/lib/web/control-lease.mjs +204 -0
- package/lib/web/http-routes.mjs +197 -15
- package/lib/web/http.mjs +5 -4
- package/lib/web/native-sessions.mjs +286 -0
- package/lib/web/project-contexts.mjs +2 -1
- package/lib/web/pty-sessions.mjs +60 -33
- package/lib/web/runtime-main.mjs +98 -6
- package/lib/web/session-serialize.mjs +0 -0
- package/lib/web/task-results.mjs +76 -0
- package/lib/web/tmux-sessions.mjs +92 -11
- package/lib/web/tmux-stream.mjs +2 -2
- package/lib/web/ui-codex.mjs +550 -0
- package/lib/web/ui-command-palette.mjs +119 -0
- package/lib/web/ui-handoff.mjs +102 -0
- package/lib/web/ui-history.mjs +118 -0
- package/lib/web/ui-i18n.mjs +841 -0
- package/lib/web/ui-interactions.mjs +78 -0
- package/lib/web/ui-login.mjs +101 -0
- package/lib/web/ui-native.mjs +165 -0
- package/lib/web/ui-preferences.mjs +278 -0
- package/lib/web/ui-review.mjs +132 -0
- package/lib/web/ui-template.mjs +1106 -576
- package/lib/web/ui-terminal-find.mjs +98 -0
- package/lib/web/ui-workbench.mjs +94 -0
- package/lib/web/ui-workspace.mjs +222 -0
- package/package.json +11 -3
- package/scripts/check-free-identifiers.mjs +67 -8
- package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
- package/scripts/dsh-cordis-acceptance.mjs +200 -0
- package/scripts/dsh-installed-acceptance.mjs +247 -0
- package/scripts/dsh-official-acceptance.mjs +180 -0
- package/scripts/dsh-web-startup-acceptance.mjs +87 -0
- package/scripts/github-release.mjs +3 -1
- package/scripts/native-live-acceptance.mjs +369 -0
- package/scripts/regression.mjs +243 -89
- package/scripts/web-handoff-installed-acceptance.mjs +98 -0
- package/scripts/web-native-interaction-acceptance.mjs +204 -0
- package/scripts/web-native-model-acceptance.mjs +138 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { createNativeInteractions } from './interactions.mjs';
|
|
6
|
+
import { CliError } from '../../shared/errors.mjs';
|
|
7
|
+
|
|
8
|
+
const SESSION_MARKERS = new Set([
|
|
9
|
+
'CLAUDECODE', 'CLAUDE_CODE_SESSION_ID', 'CLAUDE_CODE_PARENT_SESSION_ID',
|
|
10
|
+
'CLAUDE_CODE_CHILD_SESSION', 'CLAUDE_CODE_AGENT_ID',
|
|
11
|
+
'CLAUDE_CODE_MESSAGING_SOCKET', 'CLAUDE_CODE_MESSAGING_TOKEN',
|
|
12
|
+
'CODEX_THREAD_ID', 'CODEX_SESSION_ID', 'CODEX_PARENT_THREAD_ID',
|
|
13
|
+
'CODEX_INTERNAL_ORIGINATOR_OVERRIDE', 'CODEX_SANDBOX',
|
|
14
|
+
'CODEX_SANDBOX_NETWORK_DISABLED', 'TMUX', 'TMUX_PANE'
|
|
15
|
+
]);
|
|
16
|
+
const COORDINATION_KEYS = ['HCC_ROOT', 'HCC_DB', 'HCC_PEER', 'HCC_NATIVE_OWNER'];
|
|
17
|
+
|
|
18
|
+
function isolatedEnv(source, coordinationEnv = source) {
|
|
19
|
+
const clean = Object.fromEntries(Object.entries(source).filter((entry) =>
|
|
20
|
+
!entry[0].startsWith('HCC_') && !SESSION_MARKERS.has(entry[0])));
|
|
21
|
+
// Only a complete, explicitly owned worker identity can reach its hooks.
|
|
22
|
+
// An inherited terminal's partial HCC markers must not identify the worker.
|
|
23
|
+
if (COORDINATION_KEYS.every((key) => typeof coordinationEnv[key] === 'string' && coordinationEnv[key])) {
|
|
24
|
+
for (const key of COORDINATION_KEYS) clean[key] = coordinationEnv[key];
|
|
25
|
+
}
|
|
26
|
+
return clean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function inputQueue(onConsume) {
|
|
30
|
+
const values = [];
|
|
31
|
+
let waiting = null;
|
|
32
|
+
let ended = false;
|
|
33
|
+
function push(value) {
|
|
34
|
+
if (ended) throw new CliError('NATIVE_CLOSED', 'Claude native session is closed');
|
|
35
|
+
if (waiting) {
|
|
36
|
+
const resolve = waiting;
|
|
37
|
+
waiting = null;
|
|
38
|
+
onConsume(value);
|
|
39
|
+
resolve({ value, done: false });
|
|
40
|
+
} else values.push(value);
|
|
41
|
+
}
|
|
42
|
+
function end() {
|
|
43
|
+
ended = true;
|
|
44
|
+
values.length = 0;
|
|
45
|
+
if (waiting) waiting({ done: true });
|
|
46
|
+
waiting = null;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
push, end,
|
|
50
|
+
[Symbol.asyncIterator]() { return this; },
|
|
51
|
+
next() {
|
|
52
|
+
if (values.length) {
|
|
53
|
+
const value = values.shift();
|
|
54
|
+
onConsume(value);
|
|
55
|
+
return Promise.resolve({ value, done: false });
|
|
56
|
+
}
|
|
57
|
+
if (ended) return Promise.resolve({ done: true });
|
|
58
|
+
return new Promise((resolve) => { waiting = resolve; });
|
|
59
|
+
},
|
|
60
|
+
return() {
|
|
61
|
+
this.end();
|
|
62
|
+
return Promise.resolve({ done: true });
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function textContent(content) {
|
|
68
|
+
if (typeof content === 'string') return content;
|
|
69
|
+
return (Array.isArray(content) ? content : [])
|
|
70
|
+
.filter((block) => block.type === 'text')
|
|
71
|
+
.map((block) => block.text || '').join('');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function withTimeout(promise, timeoutMs, operation) {
|
|
75
|
+
let timer;
|
|
76
|
+
return Promise.race([
|
|
77
|
+
promise,
|
|
78
|
+
new Promise((_, reject) => {
|
|
79
|
+
timer = setTimeout(() => reject(new CliError('NATIVE_TIMEOUT',
|
|
80
|
+
`Claude native ${operation} timed out`, { uncertain: true })), timeoutMs);
|
|
81
|
+
})
|
|
82
|
+
]).finally(() => clearTimeout(timer));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// This adapter owns an SDK subprocess. It does not attach to a running TUI.
|
|
86
|
+
export function createClaudeAdapter(options = {}) {
|
|
87
|
+
const capabilities = Object.freeze({
|
|
88
|
+
create: true, resume: true, send: true, observe: true,
|
|
89
|
+
interrupt: true, close: true, steer: false, fork: false, approvals: Boolean(options.interactive), mcp: Boolean(options.mcpServers)
|
|
90
|
+
});
|
|
91
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
92
|
+
const env = isolatedEnv(options.env || process.env, options.coordinationEnv || options.env || {});
|
|
93
|
+
let status = 'created';
|
|
94
|
+
let sessionId = null;
|
|
95
|
+
let requestedSessionId = null;
|
|
96
|
+
let model;
|
|
97
|
+
let queryFunction = options.query;
|
|
98
|
+
let sdkQuery = null;
|
|
99
|
+
let pump = null;
|
|
100
|
+
let activeTurn = null;
|
|
101
|
+
let opening = null;
|
|
102
|
+
let closed = false;
|
|
103
|
+
let queryEnded = false;
|
|
104
|
+
let startupPromise = null;
|
|
105
|
+
let teardownPromise = null;
|
|
106
|
+
let closePromise = null;
|
|
107
|
+
const interactions = createNativeInteractions({ executorId: options.executorId,
|
|
108
|
+
isActive: (id, turnId) => !closed && id === sessionId && turnId === activeTurn?.id && !activeTurn?.interrupted && !queryEnded,
|
|
109
|
+
onChange: event => emit(event.type, event) });
|
|
110
|
+
|
|
111
|
+
function snapshot() {
|
|
112
|
+
return {
|
|
113
|
+
provider: 'claude', status, sessionId,
|
|
114
|
+
turnId: activeTurn?.id || null,
|
|
115
|
+
submissionId: activeTurn?.submissionId || null,
|
|
116
|
+
capabilities, executorId: interactions.executorId, pendingApprovals: interactions.snapshot()
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function emit(type, fields = {}, turn = activeTurn) {
|
|
121
|
+
const event = {
|
|
122
|
+
type, provider: 'claude', sessionId,
|
|
123
|
+
turnId: turn?.id || null, submissionId: turn?.submissionId || null,
|
|
124
|
+
...fields
|
|
125
|
+
};
|
|
126
|
+
// An event consumer must not break the SDK transport or permission reply.
|
|
127
|
+
try {
|
|
128
|
+
const delivery = options.onEvent?.(event);
|
|
129
|
+
delivery?.catch?.(() => {});
|
|
130
|
+
} catch {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function setStatus(next, fields = {}, turn = activeTurn) {
|
|
134
|
+
status = next;
|
|
135
|
+
emit('status', { status: next, ...fields }, turn);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const inputs = inputQueue((input) => {
|
|
139
|
+
if (activeTurn?.inputId === input.uuid && !closed) {
|
|
140
|
+
activeTurn.consumed = true;
|
|
141
|
+
setStatus('running');
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
function ownsMessage(message, turn) {
|
|
146
|
+
if (!turn || message.parent_tool_use_id) return false;
|
|
147
|
+
const ids = [message.user_message_uuid, ...(message.user_message_uuids || [])]
|
|
148
|
+
.filter(Boolean);
|
|
149
|
+
if (ids.length) return ids.includes(turn.inputId);
|
|
150
|
+
// Background task / peer turns cannot complete our pending submission.
|
|
151
|
+
if (message.origin?.kind === 'task-notification' || message.origin?.kind === 'peer') return false;
|
|
152
|
+
return turn.consumed;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function failTurn(error, turn = activeTurn) {
|
|
156
|
+
interactions.expire(turn?.id);
|
|
157
|
+
emit('error', {
|
|
158
|
+
code: error.code || 'NATIVE_SDK_ERROR', message: error.message
|
|
159
|
+
}, turn);
|
|
160
|
+
if (turn && activeTurn === turn) {
|
|
161
|
+
activeTurn = null;
|
|
162
|
+
setStatus('error', { error: error.message }, turn);
|
|
163
|
+
} else if (!closed) setStatus('error', { error: error.message }, turn);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function handleMessage(message) {
|
|
167
|
+
if (closed || queryEnded) return;
|
|
168
|
+
if (message.type === 'system' && message.subtype === 'init') {
|
|
169
|
+
const initializedId = message.session_id;
|
|
170
|
+
if (typeof initializedId !== 'string' || !initializedId.trim() ||
|
|
171
|
+
/[\x00-\x1f\x7f]/.test(initializedId) ||
|
|
172
|
+
(requestedSessionId && initializedId !== requestedSessionId)) {
|
|
173
|
+
queryEnded = true;
|
|
174
|
+
inputs.end();
|
|
175
|
+
failTurn(new CliError('NATIVE_SESSION_MISMATCH',
|
|
176
|
+
'Claude initialization did not confirm the requested valid session; refusing replacement session'));
|
|
177
|
+
void teardownQuery().catch((error) => emit('error', {
|
|
178
|
+
code: error.code || 'NATIVE_SDK_ERROR', message: error.message
|
|
179
|
+
}));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
sessionId = initializedId;
|
|
183
|
+
emit('status', { status, initialized: Boolean(sessionId) });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const turn = activeTurn;
|
|
187
|
+
const belongs = ownsMessage(message, turn);
|
|
188
|
+
if (message.type === 'assistant') {
|
|
189
|
+
const text = textContent(message.message?.content);
|
|
190
|
+
if (text) {
|
|
191
|
+
if (belongs) turn.lastText = text;
|
|
192
|
+
emit('message', {
|
|
193
|
+
text, role: 'assistant', messageId: message.uuid || message.message?.id,
|
|
194
|
+
parentToolUseId: message.parent_tool_use_id || null
|
|
195
|
+
}, belongs ? turn : null);
|
|
196
|
+
}
|
|
197
|
+
if (message.error && belongs) emit('error', {
|
|
198
|
+
code: message.error, message: text || `Claude SDK assistant error: ${message.error}`
|
|
199
|
+
}, turn);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (message.type === 'stream_event') {
|
|
203
|
+
const delta = message.event?.delta;
|
|
204
|
+
if (delta?.type === 'text_delta' && delta.text) emit('delta', {
|
|
205
|
+
text: delta.text, parentToolUseId: message.parent_tool_use_id || null
|
|
206
|
+
}, belongs ? turn : null);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (message.type !== 'result') return;
|
|
210
|
+
if (belongs) interactions.expire(turn.id);
|
|
211
|
+
if (!belongs) {
|
|
212
|
+
// Startup errors may precede input consumption and have no UUID echo.
|
|
213
|
+
if (turn && !message.parent_tool_use_id && !message.user_message_uuid &&
|
|
214
|
+
!message.origin && (message.is_error || message.subtype !== 'success')) {
|
|
215
|
+
queryEnded = true;
|
|
216
|
+
inputs.end();
|
|
217
|
+
emit('completed', { status: 'failed', text:
|
|
218
|
+
(message.errors || []).join('; ') || message.result || message.subtype }, turn);
|
|
219
|
+
failTurn(new CliError('NATIVE_SDK_RESULT_ERROR',
|
|
220
|
+
(message.errors || []).join('; ') || message.result || message.subtype), turn);
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (message.is_error || message.subtype !== 'success') {
|
|
225
|
+
emit('completed', { status: 'failed', text:
|
|
226
|
+
(message.errors || []).join('; ') || message.result || message.subtype }, turn);
|
|
227
|
+
failTurn(new CliError('NATIVE_SDK_RESULT_ERROR',
|
|
228
|
+
(message.errors || []).join('; ') || message.result || message.subtype), turn);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (message.result && message.result !== turn.lastText) emit('message', {
|
|
232
|
+
text: message.result, role: 'assistant', final: true
|
|
233
|
+
}, turn);
|
|
234
|
+
activeTurn = null;
|
|
235
|
+
const completion = /^aborted_/.test(message.terminal_reason || '')
|
|
236
|
+
? 'interrupted' : 'completed';
|
|
237
|
+
setStatus(completion, {}, turn);
|
|
238
|
+
emit('completed', { status: completion, text: message.result || turn.lastText || '' }, turn);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function open(request = {}) {
|
|
242
|
+
const resumeId = request.sessionId;
|
|
243
|
+
const selectedModel = request.model;
|
|
244
|
+
if (closed) throw new CliError('NATIVE_CLOSED', 'Claude native session is closed');
|
|
245
|
+
if (opening) return opening;
|
|
246
|
+
if (status !== 'created') {
|
|
247
|
+
if ((resumeId && resumeId !== requestedSessionId) ||
|
|
248
|
+
(selectedModel && selectedModel !== model)) {
|
|
249
|
+
throw new CliError('NATIVE_ALREADY_OPEN', 'Claude adapter already owns a different session');
|
|
250
|
+
}
|
|
251
|
+
return snapshot();
|
|
252
|
+
}
|
|
253
|
+
opening = (async () => {
|
|
254
|
+
if (queryFunction === undefined) {
|
|
255
|
+
try {
|
|
256
|
+
let entry;
|
|
257
|
+
try { entry = import.meta.resolve('@anthropic-ai/claude-agent-sdk'); }
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (error.code !== 'ERR_MODULE_NOT_FOUND') throw error;
|
|
260
|
+
// A globally installed HCC can use the optional SDK installed in
|
|
261
|
+
// this worker's project. Resolve it without changing global state.
|
|
262
|
+
const projectRequire = createRequire(path.join(path.resolve(options.cwd || process.cwd()), 'package.json'));
|
|
263
|
+
try { entry = pathToFileURL(projectRequire.resolve('@anthropic-ai/claude-agent-sdk')).href; }
|
|
264
|
+
catch (projectError) {
|
|
265
|
+
if (projectError.code !== 'MODULE_NOT_FOUND') throw projectError;
|
|
266
|
+
throw new CliError('NATIVE_SDK_MISSING',
|
|
267
|
+
'Claude native mode requires @anthropic-ai/claude-agent-sdk. In the worker project, run npm install @anthropic-ai/claude-agent-sdk; no package was installed automatically.');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const sdk = await import(entry);
|
|
271
|
+
queryFunction = sdk.query;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
// A found SDK with broken exports/dependencies is not a missing SDK.
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (typeof queryFunction !== 'function') {
|
|
278
|
+
throw new CliError('NATIVE_SDK_INVALID', 'Claude Agent SDK must expose a query function');
|
|
279
|
+
}
|
|
280
|
+
if (closed) throw new CliError('NATIVE_CLOSED', 'Claude native session is closed');
|
|
281
|
+
requestedSessionId = resumeId || null;
|
|
282
|
+
model = selectedModel;
|
|
283
|
+
// The SDK initializes after the first prompt. A requested resume ID is
|
|
284
|
+
// not reported as an initialized session before the init event arrives.
|
|
285
|
+
setStatus('ready', { initialized: false });
|
|
286
|
+
return snapshot();
|
|
287
|
+
})();
|
|
288
|
+
try { return await opening; } finally { opening = null; }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function teardownQuery() {
|
|
292
|
+
if (!sdkQuery) return Promise.resolve();
|
|
293
|
+
if (!teardownPromise) {
|
|
294
|
+
const teardown = Promise.resolve().then(() => sdkQuery.close?.());
|
|
295
|
+
teardownPromise = teardown;
|
|
296
|
+
// Share an in-flight teardown, but allow an explicit retry after failure.
|
|
297
|
+
void teardown.catch(() => {
|
|
298
|
+
if (teardownPromise === teardown) teardownPromise = null;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return teardownPromise;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function startQuery() {
|
|
305
|
+
if (closed) return Promise.reject(new CliError('NATIVE_CLOSED', 'Claude native session is closed'));
|
|
306
|
+
if (startupPromise) return startupPromise;
|
|
307
|
+
// Assign before invoking the factory so close() can observe and await any
|
|
308
|
+
// in-progress resource creation, including factories that resolve late.
|
|
309
|
+
startupPromise = Promise.resolve().then(async () => {
|
|
310
|
+
sdkQuery = await queryFunction({
|
|
311
|
+
prompt: inputs,
|
|
312
|
+
options: {
|
|
313
|
+
cwd: options.cwd, env, model,
|
|
314
|
+
...(options.mcpServers ? { mcpServers: options.mcpServers } : {}),
|
|
315
|
+
...(requestedSessionId ? { resume: requestedSessionId } : {}),
|
|
316
|
+
permissionMode: 'default',
|
|
317
|
+
allowDangerouslySkipPermissions: false,
|
|
318
|
+
includePartialMessages: true,
|
|
319
|
+
canUseTool: async (tool, input, permission = {}) => {
|
|
320
|
+
if (options.interactive && activeTurn && sessionId && !permission.parentToolUseID) {
|
|
321
|
+
return interactions.request({ requestId: permission.requestId || permission.toolUseID || randomUUID(),
|
|
322
|
+
sessionId, turnId: activeTurn.id, method: 'claude/canUseTool', params: { tool, input }, signal: permission.signal,
|
|
323
|
+
cancelled: { behavior: 'deny', message: 'Request expired or cancelled' },
|
|
324
|
+
validate: (request, response) => {
|
|
325
|
+
if (!['accept', 'decline', 'cancel'].includes(response.decision) || (request.truncated && response.decision === 'accept')) throw new CliError('INTERACTION_RESPONSE_INVALID', 'Invalid or truncated Claude approval');
|
|
326
|
+
return response.decision === 'accept' ? { behavior: 'allow', updatedInput: input }
|
|
327
|
+
: { behavior: 'deny', message: 'User declined this operation' };
|
|
328
|
+
} });
|
|
329
|
+
}
|
|
330
|
+
emit('approval', {
|
|
331
|
+
requestId: permission.requestId || permission.toolUseID || randomUUID(),
|
|
332
|
+
tool, input, status: 'denied', decision: 'deny',
|
|
333
|
+
message: 'HCC native mode does not approve permission requests automatically.'
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
behavior: 'deny',
|
|
337
|
+
message: 'Approval is required; HCC native mode has no approval responder.',
|
|
338
|
+
...(permission.toolUseID ? { toolUseID: permission.toolUseID } : {})
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
if (!sdkQuery?.[Symbol.asyncIterator]) {
|
|
344
|
+
queryEnded = true;
|
|
345
|
+
throw new CliError('NATIVE_SDK_INVALID', 'Claude query must return an asynchronous message stream');
|
|
346
|
+
}
|
|
347
|
+
// close() already owns the cleanup and awaits this startup promise.
|
|
348
|
+
if (closed) return;
|
|
349
|
+
pump = (async () => {
|
|
350
|
+
try {
|
|
351
|
+
for await (const message of sdkQuery) handleMessage(message);
|
|
352
|
+
queryEnded = true;
|
|
353
|
+
if (!closed && activeTurn) failTurn(new CliError('NATIVE_SDK_ENDED',
|
|
354
|
+
'Claude SDK stream ended before the active submission completed'));
|
|
355
|
+
else if (!closed && status !== 'error') setStatus('stopped');
|
|
356
|
+
} catch (error) {
|
|
357
|
+
queryEnded = true;
|
|
358
|
+
if (!closed) failTurn(error);
|
|
359
|
+
}
|
|
360
|
+
})();
|
|
361
|
+
});
|
|
362
|
+
return startupPromise;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function send({ text, submissionId, expectedTurnId } = {}) {
|
|
366
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
367
|
+
throw new CliError('BAD_ARGS', 'Claude native send requires non-empty text');
|
|
368
|
+
}
|
|
369
|
+
if (status === 'created') await open();
|
|
370
|
+
if (closed) throw new CliError('NATIVE_CLOSED', 'Claude native session is closed');
|
|
371
|
+
if (activeTurn) throw new CliError('NATIVE_BUSY',
|
|
372
|
+
'Claude native session is busy; steering is unsupported. Wait for completion or interrupt it.');
|
|
373
|
+
if (expectedTurnId) throw new CliError('NATIVE_STALE_TURN', 'Claude native session has no active turn to steer');
|
|
374
|
+
if (queryEnded) throw new CliError('NATIVE_SDK_ENDED', 'Claude SDK stream has ended; open a new adapter to resume');
|
|
375
|
+
const turn = {
|
|
376
|
+
id: randomUUID(), inputId: randomUUID(),
|
|
377
|
+
submissionId: submissionId || randomUUID(), consumed: false,
|
|
378
|
+
lastText: null
|
|
379
|
+
};
|
|
380
|
+
activeTurn = turn;
|
|
381
|
+
setStatus('queued');
|
|
382
|
+
try {
|
|
383
|
+
await startQuery();
|
|
384
|
+
if (closed || queryEnded || activeTurn !== turn) {
|
|
385
|
+
throw new CliError('NATIVE_SDK_ENDED', 'Claude SDK ended before accepting the submission');
|
|
386
|
+
}
|
|
387
|
+
inputs.push({
|
|
388
|
+
type: 'user', uuid: turn.inputId,
|
|
389
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
390
|
+
parent_tool_use_id: null,
|
|
391
|
+
message: { role: 'user', content: text }
|
|
392
|
+
});
|
|
393
|
+
return {
|
|
394
|
+
sessionId, turnId: turn.id, submissionId: turn.submissionId,
|
|
395
|
+
// Consuming the iterator is a local SDK handoff, not provider admission.
|
|
396
|
+
status: 'queued'
|
|
397
|
+
};
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (activeTurn === turn) failTurn(error, turn);
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function interrupt(request = {}) {
|
|
405
|
+
const turnId = request.turnId;
|
|
406
|
+
if (closed) throw new CliError('NATIVE_CLOSED', 'Claude native session is closed');
|
|
407
|
+
const turn = activeTurn;
|
|
408
|
+
if (!turn || (turnId && turn.id !== turnId)) {
|
|
409
|
+
throw new CliError('NATIVE_STALE_TURN', 'Claude native interrupt requires the active turn');
|
|
410
|
+
}
|
|
411
|
+
if (!sdkQuery?.interrupt) throw new CliError('NATIVE_UNSUPPORTED', 'Claude SDK does not support interruption');
|
|
412
|
+
// interrupt() does not cancel queued SDK input. Keep the reservation until
|
|
413
|
+
// a result arrives; never mark an unconsumed queued submission completed.
|
|
414
|
+
const receipt = await withTimeout(Promise.resolve(sdkQuery.interrupt()), timeoutMs, 'interrupt');
|
|
415
|
+
// Keep the turn reserved until its result arrives, but never reopen a
|
|
416
|
+
// human approval after the provider accepted this interruption.
|
|
417
|
+
turn.interrupted = true;
|
|
418
|
+
interactions.expire(turn.id, 'interrupted');
|
|
419
|
+
if (activeTurn === turn) emit('status', { status: 'interrupt_requested' }, turn);
|
|
420
|
+
return { sessionId, turnId: turn.id, status: 'interrupt_requested', receipt };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function close() {
|
|
424
|
+
if (closePromise) return closePromise;
|
|
425
|
+
closed = true;
|
|
426
|
+
interactions.expire();
|
|
427
|
+
const turn = activeTurn;
|
|
428
|
+
activeTurn = null;
|
|
429
|
+
inputs.end();
|
|
430
|
+
setStatus('closing', {}, turn);
|
|
431
|
+
// A deadline bounds callers, while the cleanup keeps waiting for a late
|
|
432
|
+
// factory and tears down its resource even if the caller already timed out.
|
|
433
|
+
const cleanup = (async () => {
|
|
434
|
+
if (opening) await opening.catch(() => {});
|
|
435
|
+
if (startupPromise) await startupPromise.catch(() => {});
|
|
436
|
+
await teardownQuery();
|
|
437
|
+
if (pump) await pump;
|
|
438
|
+
setStatus('closed', turn ? { cancelled: true } : {}, turn);
|
|
439
|
+
return snapshot();
|
|
440
|
+
})();
|
|
441
|
+
closePromise = withTimeout(cleanup, timeoutMs, 'close').catch((error) => {
|
|
442
|
+
// A caller may retry shutdown after the deadline or a teardown failure.
|
|
443
|
+
closePromise = null;
|
|
444
|
+
setStatus('error', { error: error.message, uncertain: true }, turn);
|
|
445
|
+
throw error;
|
|
446
|
+
});
|
|
447
|
+
return closePromise;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return { capabilities, open, send, interrupt, close, snapshot, respond: interactions.respond };
|
|
451
|
+
}
|