@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.
Files changed (97) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +4 -0
  3. package/README.zh-CN.md +4 -0
  4. package/docs/README.md +9 -0
  5. package/docs/README.zh-CN.md +9 -0
  6. package/docs/commands.md +53 -0
  7. package/docs/commands.zh-CN.md +48 -0
  8. package/docs/dsh.md +137 -0
  9. package/docs/dsh.zh-CN.md +139 -0
  10. package/docs/mcp.md +9 -0
  11. package/docs/mcp.zh-CN.md +23 -0
  12. package/docs/native.md +295 -0
  13. package/docs/native.zh-CN.md +253 -0
  14. package/docs/web-handoff.zh-CN.md +53 -0
  15. package/lib/cli/app.mjs +25 -6
  16. package/lib/cli/commands/doctor.mjs +17 -2
  17. package/lib/cli/commands/dsh.mjs +79 -0
  18. package/lib/cli/commands/hook.mjs +241 -90
  19. package/lib/cli/commands/lock.mjs +34 -19
  20. package/lib/cli/commands/mcp.mjs +28 -0
  21. package/lib/cli/commands/native.mjs +130 -0
  22. package/lib/cli/commands/run.mjs +66 -21
  23. package/lib/cli/commands/task.mjs +6 -3
  24. package/lib/cli-args.mjs +4 -0
  25. package/lib/coordination-state.mjs +15 -1
  26. package/lib/core/coordination/event-retention.mjs +14 -2
  27. package/lib/core/coordination/gc-plan.mjs +3 -13
  28. package/lib/core/coordination/locks.mjs +45 -0
  29. package/lib/core/coordination/results.mjs +79 -0
  30. package/lib/core/peers/bindings.mjs +9 -3
  31. package/lib/diagnostics/codex.mjs +121 -0
  32. package/lib/integrations/codex-interactions.mjs +114 -0
  33. package/lib/integrations/dsh-collaboration.mjs +170 -0
  34. package/lib/integrations/dsh-cordis.d.ts +19 -0
  35. package/lib/integrations/dsh-cordis.mjs +177 -0
  36. package/lib/integrations/dsh.bundle.yml +7 -0
  37. package/lib/integrations/dsh.mjs +287 -0
  38. package/lib/integrations/native/claude.mjs +451 -0
  39. package/lib/integrations/native/codex.mjs +242 -0
  40. package/lib/integrations/native/dsh-acp.mjs +343 -0
  41. package/lib/integrations/native/index.mjs +19 -0
  42. package/lib/integrations/native/interactions.mjs +53 -0
  43. package/lib/integrations/native/jsonrpc.mjs +185 -0
  44. package/lib/integrations/peers/identity.mjs +36 -10
  45. package/lib/integrations/peers/processes.mjs +22 -8
  46. package/lib/integrations/providers.mjs +6 -2
  47. package/lib/mcp/scope.mjs +104 -0
  48. package/lib/mcp/stdio.mjs +80 -0
  49. package/lib/mcp/tools.mjs +197 -0
  50. package/lib/process/identity.mjs +3 -1
  51. package/lib/release/release-notes.mjs +9 -0
  52. package/lib/runtime/native/client.mjs +38 -0
  53. package/lib/runtime/native/service.mjs +511 -0
  54. package/lib/runtime/native/store.mjs +121 -0
  55. package/lib/ui/help.mjs +5 -2
  56. package/lib/web/adoption-state.mjs +55 -0
  57. package/lib/web/auto-attach.mjs +18 -2
  58. package/lib/web/codex-app-server.mjs +557 -0
  59. package/lib/web/codex-sessions.mjs +287 -0
  60. package/lib/web/control-lease.mjs +204 -0
  61. package/lib/web/http-routes.mjs +197 -15
  62. package/lib/web/http.mjs +5 -4
  63. package/lib/web/native-sessions.mjs +286 -0
  64. package/lib/web/project-contexts.mjs +2 -1
  65. package/lib/web/pty-sessions.mjs +60 -33
  66. package/lib/web/runtime-main.mjs +98 -6
  67. package/lib/web/session-serialize.mjs +0 -0
  68. package/lib/web/task-results.mjs +76 -0
  69. package/lib/web/tmux-sessions.mjs +92 -11
  70. package/lib/web/tmux-stream.mjs +2 -2
  71. package/lib/web/ui-codex.mjs +550 -0
  72. package/lib/web/ui-command-palette.mjs +119 -0
  73. package/lib/web/ui-handoff.mjs +102 -0
  74. package/lib/web/ui-history.mjs +118 -0
  75. package/lib/web/ui-i18n.mjs +841 -0
  76. package/lib/web/ui-interactions.mjs +78 -0
  77. package/lib/web/ui-login.mjs +101 -0
  78. package/lib/web/ui-native.mjs +165 -0
  79. package/lib/web/ui-preferences.mjs +278 -0
  80. package/lib/web/ui-review.mjs +132 -0
  81. package/lib/web/ui-template.mjs +1106 -576
  82. package/lib/web/ui-terminal-find.mjs +98 -0
  83. package/lib/web/ui-workbench.mjs +94 -0
  84. package/lib/web/ui-workspace.mjs +222 -0
  85. package/package.json +11 -3
  86. package/scripts/check-free-identifiers.mjs +67 -8
  87. package/scripts/codex-diagnostics-installed-acceptance.mjs +72 -0
  88. package/scripts/dsh-cordis-acceptance.mjs +200 -0
  89. package/scripts/dsh-installed-acceptance.mjs +247 -0
  90. package/scripts/dsh-official-acceptance.mjs +180 -0
  91. package/scripts/dsh-web-startup-acceptance.mjs +87 -0
  92. package/scripts/github-release.mjs +3 -1
  93. package/scripts/native-live-acceptance.mjs +369 -0
  94. package/scripts/regression.mjs +243 -89
  95. package/scripts/web-handoff-installed-acceptance.mjs +98 -0
  96. package/scripts/web-native-interaction-acceptance.mjs +204 -0
  97. package/scripts/web-native-model-acceptance.mjs +138 -0
@@ -0,0 +1,287 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import { createHash, randomUUID } from 'node:crypto';
5
+ import { spawn } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { CliError } from '../shared/errors.mjs';
8
+ import { withFileLock } from '../shared/file-lock.mjs';
9
+ import { commandPath } from '../cli-runtime.mjs';
10
+ import { shellQuoteArg } from '../format.mjs';
11
+
12
+ export const DSH_BASELINE_VERSION = '0.2.0-rc.2';
13
+ export const DSH_INSTALL_HINT = `npm install -g @deepseek-ai/dsh@${DSH_BASELINE_VERSION}`;
14
+ export const DSH_HOOK_EVENTS = Object.freeze([
15
+ 'SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop'
16
+ ]);
17
+ const MANAGED_BY = 'hello-cc/dsh';
18
+ const ARTIFACTS = ['hooks.json', 'cordis.patch.yml', 'managed.json'];
19
+ const SESSION_MARKERS = new Set([
20
+ 'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT', 'CLAUDE_SESSION_ID',
21
+ 'CLAUDE_CODE_SESSION_ID', 'CLAUDE_CONVERSATION_ID', 'CLAUDE_PROJECT_DIR', 'CLAUDE_PLUGIN_ROOT',
22
+ 'CODEX_THREAD_ID', 'CODEX_SESSION_ID', 'CODEX_CONVERSATION_ID',
23
+ 'CODEX_PARENT_THREAD_ID', 'CODEX_PROJECT_DIR', 'CODEX_MANAGED_BY_NPM', 'CODEX_MANAGED_BY_BUN'
24
+ ]);
25
+
26
+ function forbidden(file, reason) {
27
+ return new CliError('DSH_CONFIG_CONFLICT',
28
+ `Refusing to modify dsh integration at ${file}: ${reason}. Preserve or move the existing file, then run hcc dsh setup.`);
29
+ }
30
+
31
+ function lstat(file) {
32
+ try { return fs.lstatSync(file); }
33
+ catch (error) { if (error.code === 'ENOENT') return null; throw error; }
34
+ }
35
+
36
+ function sameObject(left, right) {
37
+ return left?.dev === right?.dev && left?.ino === right?.ino;
38
+ }
39
+
40
+ function integrationPaths(ctx, create = false) {
41
+ let root;
42
+ try { root = fs.realpathSync(ctx.root); }
43
+ catch { throw new CliError('BAD_ARGS', `Project root does not exist: ${ctx.root}`); }
44
+ if (!fs.statSync(root).isDirectory()) throw new CliError('BAD_ARGS', `Project root is not a directory: ${root}`);
45
+ const stateDir = path.join(root, '.hello-cc');
46
+ const directory = path.join(stateDir, 'dsh');
47
+ const parents = [];
48
+ for (const dir of [root, stateDir, directory]) {
49
+ let stat = lstat(dir);
50
+ if (!stat && create) {
51
+ try { fs.mkdirSync(dir, { mode: 0o700 }); }
52
+ catch (error) { if (error.code !== 'EEXIST') throw error; }
53
+ stat = lstat(dir);
54
+ }
55
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) {
56
+ throw forbidden(dir, 'expected an ordinary directory, found a symlink or other object');
57
+ }
58
+ if (stat) parents.push({ file: dir, stat });
59
+ }
60
+ return { root, directory, parents, hooksPath: path.join(directory, 'hooks.json'),
61
+ patchPath: path.join(directory, 'cordis.patch.yml'), manifestPath: path.join(directory, 'managed.json') };
62
+ }
63
+
64
+ function assertParents(paths) {
65
+ for (const parent of paths.parents) {
66
+ const current = lstat(parent.file);
67
+ if (!current?.isDirectory() || current.isSymbolicLink() || !sameObject(current, parent.stat)) {
68
+ throw forbidden(parent.file, 'directory identity changed');
69
+ }
70
+ }
71
+ }
72
+
73
+ function readArtifact(paths, name) {
74
+ assertParents(paths);
75
+ const file = path.join(paths.directory, name);
76
+ const stat = lstat(file);
77
+ if (!stat) return null;
78
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
79
+ throw forbidden(file, 'expected an ordinary file with no symlink or hard-link alias');
80
+ }
81
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
82
+ try {
83
+ if (!sameObject(fs.fstatSync(fd), stat)) throw forbidden(file, 'file identity changed');
84
+ const content = fs.readFileSync(fd, 'utf8');
85
+ if (!sameObject(lstat(file), stat)) throw forbidden(file, 'file identity changed');
86
+ assertParents(paths);
87
+ return { content, stat };
88
+ } finally { fs.closeSync(fd); }
89
+ }
90
+
91
+ function jsonText(value) { return JSON.stringify(value, null, 2) + '\n'; }
92
+ function hash(content) { return createHash('sha256').update(content).digest('hex'); }
93
+
94
+ function generatedArtifacts(paths, options = {}, recorded = null) {
95
+ const mode = options.mode || 'hooks';
96
+ if (!['hooks', 'cordis', 'off'].includes(mode)) throw new CliError('BAD_ARGS', 'dsh mode must be hooks, cordis, or off');
97
+ const hccBin = path.resolve(options.hccBin || commandPath());
98
+ const nodeBin = path.resolve(options.nodeBin || process.execPath);
99
+ const hookPrefix = `${shellQuoteArg(nodeBin)} ${shellQuoteArg(hccBin)} hook`;
100
+ const hooks = {};
101
+ for (const event of DSH_HOOK_EVENTS) {
102
+ hooks[event] = [{ hooks: [{ type: 'command', command: `${hookPrefix} ${event.toLowerCase()} --provider dsh` }] }];
103
+ }
104
+ const hooksContent = jsonText({ _helloCc: { managedBy: MANAGED_BY, schema: 1 }, hooks });
105
+ // JSON-quoted strings are valid YAML scalars, including spaces, quotes and newlines.
106
+ // Omitting projectDir lets the official bridge use each session's own cwd.
107
+ const pluginPath = recorded?.pluginPath || fileURLToPath(new URL('./dsh-cordis.mjs', import.meta.url));
108
+ const pluginName = mode === 'cordis' ? pluginPath : '@deepseek-ai/dsh-hooks-claude-code';
109
+ const patchContent = mode === 'off' ? '# Generated by hello-cc. Integration disabled.\n[]\n' : [
110
+ '# Generated by hello-cc. Run hcc dsh setup to refresh; keep custom patches separate.',
111
+ '- insert:',
112
+ ` - id: hello-cc-dsh-${mode === 'cordis' ? 'cordis' : 'hooks'}`,
113
+ ` name: ${JSON.stringify(pluginName)}`,
114
+ ' config:',
115
+ ...(mode === 'cordis' ? [' maxContextChars: 16000', ' maxToolChars: 16000'] : [
116
+ ` configPath: ${JSON.stringify(paths.hooksPath)}`, ' defaultTimeoutMs: 10000']),
117
+ ''
118
+ ].join('\n');
119
+ const manifest = { managedBy: MANAGED_BY, schema: 1, baselineVersion: DSH_BASELINE_VERSION,
120
+ root: paths.root, hccBin, nodeBin,
121
+ ...(options.mode !== undefined ? { mode } : {}),
122
+ ...(mode === 'cordis' && !recorded?.legacy ? { pluginPath } : {}),
123
+ files: { 'hooks.json': hash(hooksContent), 'cordis.patch.yml': hash(patchContent) } };
124
+ return { 'hooks.json': hooksContent, 'cordis.patch.yml': patchContent, 'managed.json': jsonText(manifest) };
125
+ }
126
+
127
+ function inspectArtifacts(paths, desired) {
128
+ const existing = {};
129
+ for (const name of ARTIFACTS) existing[name] = readArtifact(paths, name);
130
+ let recorded = desired;
131
+ if (existing['managed.json']) {
132
+ let manifest;
133
+ try { manifest = JSON.parse(existing['managed.json'].content); }
134
+ catch { throw forbidden(paths.manifestPath, 'managed manifest is invalid JSON'); }
135
+ if (manifest?.managedBy !== MANAGED_BY || manifest?.schema !== 1 ||
136
+ manifest?.baselineVersion !== DSH_BASELINE_VERSION || manifest?.root !== paths.root ||
137
+ (manifest.mode !== undefined && !['hooks', 'cordis', 'off'].includes(manifest.mode)) ||
138
+ typeof manifest?.hccBin !== 'string' || !path.isAbsolute(manifest.hccBin) ||
139
+ typeof manifest?.nodeBin !== 'string' || !path.isAbsolute(manifest.nodeBin) ||
140
+ (manifest.pluginPath !== undefined && (typeof manifest.pluginPath !== 'string' || !path.isAbsolute(manifest.pluginPath)))) {
141
+ throw forbidden(paths.manifestPath, 'managed manifest has foreign or unsupported ownership');
142
+ }
143
+ let pluginPath = manifest.pluginPath;
144
+ if (manifest.mode === 'cordis' && pluginPath === undefined) {
145
+ // Schema-1 manifests predate pluginPath. Recover their module location
146
+ // only from an unchanged hash-bound overlay, then verify its full template.
147
+ const overlay = existing['cordis.patch.yml'];
148
+ if (overlay) {
149
+ if (manifest.files?.['cordis.patch.yml'] !== hash(overlay.content)) {
150
+ throw forbidden(paths.patchPath, 'managed Cordis overlay was edited');
151
+ }
152
+ const scalar = overlay.content.match(/^ name: (.+)$/m)?.[1];
153
+ try { pluginPath = JSON.parse(scalar); } catch {}
154
+ if (typeof pluginPath !== 'string' || !path.isAbsolute(pluginPath)) {
155
+ throw forbidden(paths.patchPath, 'recorded Cordis module path is invalid');
156
+ }
157
+ } else if (path.basename(manifest.hccBin) === 'hcc.mjs' && path.basename(path.dirname(manifest.hccBin)) === 'bin') {
158
+ pluginPath = path.resolve(path.dirname(manifest.hccBin), '../lib/integrations/dsh-cordis.mjs');
159
+ } else {
160
+ throw forbidden(paths.manifestPath, 'legacy Cordis module location cannot be established without its overlay');
161
+ }
162
+ }
163
+ recorded = generatedArtifacts(paths, manifest, { pluginPath, legacy: manifest.pluginPath === undefined });
164
+ }
165
+ for (const name of ARTIFACTS) {
166
+ if (existing[name] && existing[name].content !== recorded[name]) {
167
+ throw forbidden(path.join(paths.directory, name), 'managed content was edited or belongs to another integration');
168
+ }
169
+ }
170
+ const missing = ARTIFACTS.filter((name) => !existing[name]);
171
+ const current = missing.length === 0 && ARTIFACTS.every((name) => existing[name].content === desired[name]);
172
+ return { existing, missing, current };
173
+ }
174
+
175
+ function writeArtifact(paths, name, content, previous) {
176
+ const file = path.join(paths.directory, name);
177
+ const tmp = path.join(paths.directory, `.${name}.${randomUUID()}.tmp`);
178
+ try {
179
+ assertParents(paths);
180
+ fs.writeFileSync(tmp, content, { flag: 'wx', mode: 0o600 });
181
+ const current = readArtifact(paths, name);
182
+ if (Boolean(current) !== Boolean(previous) || (current &&
183
+ (!sameObject(current.stat, previous.stat) || current.content !== previous.content))) {
184
+ throw forbidden(file, 'file changed during setup');
185
+ }
186
+ assertParents(paths);
187
+ fs.renameSync(tmp, file);
188
+ } finally {
189
+ assertParents(paths);
190
+ try { fs.unlinkSync(tmp); } catch (error) { if (error.code !== 'ENOENT') throw error; }
191
+ }
192
+ }
193
+
194
+ function selectedOptions(paths, options) {
195
+ if (options.mode !== undefined) return options;
196
+ const existing = readArtifact(paths, 'managed.json');
197
+ if (!existing) return { ...options, mode: 'hooks' };
198
+ try { return { ...options, mode: JSON.parse(existing.content).mode }; }
199
+ catch { return options; } // Ownership/JSON errors are reported by inspectArtifacts.
200
+ }
201
+
202
+ export function ensureDshIntegration(ctx, options = {}) {
203
+ const paths = integrationPaths(ctx, true);
204
+ return withFileLock(paths.manifestPath, () => {
205
+ options = selectedOptions(paths, options);
206
+ const desired = generatedArtifacts(paths, options);
207
+ const inspection = inspectArtifacts(paths, desired);
208
+ const changed = [];
209
+ for (const name of ARTIFACTS) {
210
+ if (inspection.existing[name]?.content === desired[name]) continue;
211
+ writeArtifact(paths, name, desired[name], inspection.existing[name]);
212
+ changed.push(path.join(paths.directory, name));
213
+ }
214
+ return { root: paths.root, hooksPath: paths.hooksPath, patchPath: paths.patchPath,
215
+ manifestPath: paths.manifestPath, ready: true, mode: options.mode || 'hooks', changed, baselineVersion: DSH_BASELINE_VERSION };
216
+ }, { createParent: false });
217
+ }
218
+
219
+ export function inspectDshIntegration(ctx, options = {}) {
220
+ try {
221
+ const paths = integrationPaths(ctx);
222
+ options = selectedOptions(paths, options);
223
+ const inspection = inspectArtifacts(paths, generatedArtifacts(paths, options));
224
+ return { root: paths.root, hooksPath: paths.hooksPath, patchPath: paths.patchPath,
225
+ manifestPath: paths.manifestPath, ready: inspection.current, mode: options.mode || 'hooks',
226
+ state: inspection.current ? (options.mode === 'off' ? 'disabled' : 'ready') : inspection.missing.length ? 'missing' : 'refresh-needed',
227
+ missing: inspection.missing, baselineVersion: DSH_BASELINE_VERSION };
228
+ } catch (error) {
229
+ if (!(error instanceof CliError)) throw error;
230
+ return { root: path.resolve(ctx.root), ready: false, state: 'conflict',
231
+ error: { code: error.code, message: error.message }, baselineVersion: DSH_BASELINE_VERSION };
232
+ }
233
+ }
234
+
235
+ export function resolveDshBinary(options = {}) {
236
+ const env = options.env || process.env;
237
+ const cwd = options.cwd || process.cwd();
238
+ const candidates = options.dshBin
239
+ ? [path.resolve(cwd, options.dshBin)]
240
+ : String(env.PATH || '').split(path.delimiter).filter(Boolean).map((dir) => path.resolve(cwd, dir, 'dsh'));
241
+ for (const candidate of candidates) {
242
+ try {
243
+ const real = fs.realpathSync(candidate);
244
+ if (!fs.statSync(real).isFile()) continue;
245
+ fs.accessSync(real, fs.constants.X_OK);
246
+ return real;
247
+ } catch {}
248
+ }
249
+ return null;
250
+ }
251
+
252
+ export function createDshEnvironment(env = process.env, options = {}) {
253
+ const filtered = {};
254
+ for (const name of Object.keys(env)) {
255
+ if (name.startsWith('HCC_') || SESSION_MARKERS.has(name)) continue;
256
+ if (env[name] !== undefined) filtered[name] = env[name];
257
+ }
258
+ if (options.dshHome) filtered.DSH_HOME = path.resolve(options.cwd || process.cwd(), options.dshHome);
259
+ return filtered;
260
+ }
261
+
262
+ export async function launchDshWeb(ctx, options = {}) {
263
+ const binary = resolveDshBinary({ ...options, cwd: ctx.cwd || process.cwd() });
264
+ const location = options.dshBin ? `: ${options.dshBin}` : ' on PATH';
265
+ if (!binary) throw new CliError('DSH_NOT_FOUND',
266
+ `dsh executable not found${location}. Install it with: ${DSH_INSTALL_HINT}; or pass --dsh-bin /absolute/path/to/dsh.`);
267
+ const setup = ensureDshIntegration(ctx, options);
268
+ const env = createDshEnvironment(options.env || process.env, { dshHome: options.dshHome, cwd: ctx.cwd });
269
+ const args = ['web', '--patch', setup.patchPath, ...(options.args || [])];
270
+ return new Promise((resolve, reject) => {
271
+ const child = spawn(binary, args, { cwd: setup.root, env, stdio: options.stdio || 'inherit' });
272
+ const handlers = new Map();
273
+ const cleanup = () => {
274
+ for (const signal of handlers.keys()) process.removeListener(signal, handlers.get(signal));
275
+ };
276
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
277
+ const handler = () => { if (!child.killed) child.kill(signal); };
278
+ handlers.set(signal, handler);
279
+ process.on(signal, handler);
280
+ }
281
+ child.once('error', (error) => {
282
+ cleanup();
283
+ reject(new CliError('DSH_LAUNCH_FAILED', `Could not launch dsh: ${error.message}`));
284
+ });
285
+ child.once('exit', (code, signal) => { cleanup(); resolve({ code, signal, binary, ...setup }); });
286
+ });
287
+ }