@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,98 @@
1
+ export const terminalFindCss = `
2
+ .terminal-find { flex:none; padding:8px var(--space); border-bottom:1px solid var(--border); background:var(--panel); }
3
+ .terminal-find-row { display:flex; gap:6px; align-items:center; }
4
+ .terminal-find-row input { flex:1; min-width:60px; }
5
+ .terminal-find-row button { flex:none; min-width:36px; }
6
+ .terminal-find-meta { display:flex; gap:14px; align-items:center; flex-wrap:wrap; margin-top:7px; color:var(--muted); font-size:12px; }
7
+ .terminal-find-meta label { display:flex; gap:5px; align-items:center; }
8
+ .terminal-find-meta input { width:auto; margin:0; }
9
+ #terminalFindResult { flex:1; min-width:100px; }
10
+ @media(pointer:coarse) { .terminal-find-row button { min-width:44px; min-height:44px; } }
11
+ `;
12
+
13
+ export function terminalFindHtml() {
14
+ return `<section id="terminalFind" class="terminal-find" role="search" data-i18n-aria="find.title" aria-label="Find in terminal" hidden>
15
+ <div class="terminal-find-row"><input id="terminalFindQuery" type="search" maxlength="256" autocomplete="off" data-i18n-placeholder="find.placeholder" data-i18n-aria="find.title" placeholder="Find in terminal output…" aria-label="Find in terminal"><button id="terminalFindPrev" type="button" data-i18n-title="find.previous" data-i18n-aria="find.previous" title="Previous match" aria-label="Previous match">↑</button><button id="terminalFindNext" type="button" data-i18n-title="find.next" data-i18n-aria="find.next" title="Next match" aria-label="Next match">↓</button><button id="terminalFindClose" type="button" data-i18n-title="close" data-i18n-aria="close" aria-label="Close">×</button></div>
16
+ <div class="terminal-find-meta"><label><input id="terminalFindCase" type="checkbox"><span data-i18n="find.case">Match case</span></label><label><input id="terminalFindWord" type="checkbox"><span data-i18n="find.word">Whole word</span></label><span id="terminalFindResult" role="status" aria-live="polite"></span></div>
17
+ </section>`;
18
+ }
19
+
20
+ export function terminalFindScript() {
21
+ return `(() => {
22
+ const host = window.hccTerminalFindHost;
23
+ const bar = document.getElementById('terminalFind');
24
+ const query = document.getElementById('terminalFindQuery');
25
+ const caseOption = document.getElementById('terminalFindCase');
26
+ const wordOption = document.getElementById('terminalFindWord');
27
+ const result = document.getElementById('terminalFindResult');
28
+ const prev = document.getElementById('terminalFindPrev'), next = document.getElementById('terminalFindNext');
29
+ const tr = key => window.hccUi.tr(key);
30
+ let subject = '', timer = 0, composing = false, searched = false;
31
+ let count = 0, index = -1;
32
+ function renderResult() {
33
+ const message = !query.value ? tr('find.hint') : count === 0 ? tr('find.none') : index < 0 ? tr('find.many').replace('{count}', String(count)) : tr('find.count').replace('{index}', String(index + 1)).replace('{count}', String(count));
34
+ if (result.textContent !== message) result.textContent = message;
35
+ prev.disabled = next.disabled = !query.value;
36
+ }
37
+ function close(focus = true) {
38
+ clearTimeout(timer); bar.hidden = true;
39
+ document.getElementById('terminalFindBtn').setAttribute('aria-expanded','false');
40
+ if (searched) host.addon.clearDecorations();
41
+ searched = false;
42
+ if (focus && host.available()) host.term.focus();
43
+ }
44
+ function sync() {
45
+ if (subject !== host.subject() || !host.available()) {
46
+ close(false); subject = host.subject(); query.value = ''; count = 0; index = -1;
47
+ }
48
+ document.getElementById('terminalFindBtn').disabled = !host.available();
49
+ document.getElementById('terminalFindBtn').hidden = !host.available();
50
+ renderResult();
51
+ }
52
+ function options(incremental) {
53
+ const css = getComputedStyle(document.documentElement);
54
+ const color = name => css.getPropertyValue(name).trim();
55
+ return {caseSensitive:caseOption.checked, wholeWord:wordOption.checked, incremental, regex:false,
56
+ decorations:{matchBackground:color('--selection-bg'), matchBorder:color('--border'), matchOverviewRuler:color('--accent'), activeMatchBackground:color('--accent'), activeMatchBorder:color('--accent'), activeMatchColorOverviewRuler:color('--accent')}};
57
+ }
58
+ function search(direction = 1, incremental = true) {
59
+ clearTimeout(timer);
60
+ if (bar.hidden || composing || !host.available() || subject !== host.subject()) return;
61
+ if (!query.value) { if (searched) host.addon.clearDecorations(); searched = false; count = 0; index = -1; renderResult(); return; }
62
+ searched = true;
63
+ try {
64
+ const found = direction < 0 ? host.addon.findPrevious(query.value, options(false)) : host.addon.findNext(query.value, options(incremental));
65
+ if (!found) { count = 0; index = -1; }
66
+ renderResult();
67
+ } catch(error) { host.addon.clearDecorations(); searched = false; count = 0; index = -1; result.textContent = tr('find.failed'); prev.disabled = next.disabled = true; }
68
+ }
69
+ function schedule() { if (!composing) { clearTimeout(timer); timer = setTimeout(() => search(), 120); } }
70
+ function refreshMatches() {
71
+ // The addon caches highlights for the same query; invalidate them when
72
+ // matching options or theme colors change so counts and colors stay fresh.
73
+ if (searched) host.addon.clearDecorations();
74
+ searched = false;
75
+ search();
76
+ }
77
+ function open() {
78
+ sync(); if (!host.available()) return;
79
+ host.showTerminal(); bar.hidden = false; document.getElementById('terminalFindBtn').setAttribute('aria-expanded','true'); query.focus(); query.select(); renderResult();
80
+ if (query.value) search();
81
+ }
82
+ host.addon.onDidChangeResults(value => { if (bar.hidden) return; count = value.resultCount; index = value.resultIndex; renderResult(); });
83
+ query.addEventListener('input', event => { if (!event.isComposing) schedule(); });
84
+ query.addEventListener('compositionstart', () => { composing = true; clearTimeout(timer); });
85
+ query.addEventListener('compositionend', () => { composing = false; schedule(); });
86
+ query.addEventListener('keydown', event => {
87
+ if (event.isComposing) return;
88
+ if (event.key === 'Enter') { event.preventDefault(); event.stopPropagation(); search(event.shiftKey ? -1 : 1, false); }
89
+ });
90
+ caseOption.addEventListener('change', refreshMatches); wordOption.addEventListener('change', refreshMatches);
91
+ prev.addEventListener('click', () => search(-1, false)); next.addEventListener('click', () => search(1, false));
92
+ document.getElementById('terminalFindClose').addEventListener('click', () => close());
93
+ document.getElementById('terminalFindBtn').addEventListener('click', open);
94
+ window.addEventListener('hcc:preferences', () => { if (!bar.hidden) { renderResult(); if (query.value) refreshMatches(); } });
95
+ window.hccTerminalFind = {open, close, sync, capturesEscape:event => !bar.hidden && event.key === 'Escape' && (bar.contains(event.target) || host.term.element.contains(event.target))};
96
+ sync();
97
+ })();`;
98
+ }
@@ -0,0 +1,94 @@
1
+ export const workbenchCss = `
2
+ .inspector { grid-template-rows:auto auto auto minmax(0,1fr); }
3
+ .state-tabs { display:flex; gap:4px; padding:8px var(--space); border-bottom:1px solid var(--border); }
4
+ .state-tabs button { flex:1; min-width:0; border:0; background:transparent; color:var(--muted); padding:0 5px; font-size:12px; }
5
+ .state-tabs button[aria-selected="true"] { background:var(--selection-bg); color:var(--text); }
6
+ .state-freshness { display:flex; align-items:center; gap:8px; padding:8px var(--space); color:var(--muted); font-size:var(--small-font); }
7
+ .state-freshness span { flex:1; min-width:0; overflow-wrap:anywhere; }
8
+ .state-freshness[data-stale="true"] { color:var(--warn); background:var(--panel-2); }
9
+ .state-freshness button { height:28px; padding:0 8px; font-size:12px; }
10
+ .state-announcement { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }
11
+ .handoff-chips { display:flex; gap:6px; flex-wrap:wrap; align-items:center; }
12
+ .handoff-chip { display:inline-flex; align-items:center; gap:5px; color:var(--muted); font-size:11px; background:var(--panel-2); padding:4px 8px; border-radius:5px; }
13
+ .handoff-chip[data-tone="ok"] { color:var(--ok); }
14
+ .handoff-chip[data-tone="warn"] { color:var(--warn); }
15
+ .handoff-chip[data-tone="danger"] { color:var(--danger); }
16
+ .handoff-detail summary { cursor:pointer; width:fit-content; padding:3px 0; }
17
+ .handoff-detail div { padding-top:5px; line-height:1.6; }
18
+ @media (pointer:coarse) { .handoff-bar button, .state-tabs button, .state-freshness button { min-height:40px; } }
19
+ `;
20
+
21
+ export function workbenchControlsHtml() {
22
+ return `<nav class="state-tabs" role="tablist" aria-label="Project views" id="stateTabs">
23
+ <button type="button" id="stateTab-overview" role="tab" data-state-view="overview" aria-controls="state" aria-selected="true">Overview</button>
24
+ <button type="button" id="stateTab-tasks" role="tab" data-state-view="tasks" aria-controls="state" aria-selected="false" tabindex="-1">Tasks</button>
25
+ <button type="button" id="stateTab-messages" role="tab" data-state-view="messages" aria-controls="state" aria-selected="false" tabindex="-1">Messages</button>
26
+ <button type="button" id="stateTab-events" role="tab" data-state-view="events" aria-controls="state" aria-selected="false" tabindex="-1">Events</button>
27
+ </nav>
28
+ <div class="state-freshness" id="stateFreshness" data-stale="false"><span id="stateUpdated"></span><span id="stateAnnouncement" class="state-announcement" role="status" aria-live="polite"></span><button type="button" id="stateRetry" hidden>Retry</button></div>`;
29
+ }
30
+
31
+ // Display state only: the caller owns requests, project scope and retry policy.
32
+ export function workbenchScript() {
33
+ return `(() => {
34
+ const tabs = document.getElementById('stateTabs');
35
+ const state = document.getElementById('state');
36
+ const notice = document.getElementById('stateFreshness');
37
+ const updated = document.getElementById('stateUpdated');
38
+ const announcement = document.getElementById('stateAnnouncement');
39
+ const retry = document.getElementById('stateRetry');
40
+ const groups = { overview: ['automation', 'tasks', 'locks'], tasks: ['tasks', 'peers', 'locks'], messages: ['messages'], events: ['timeline'] };
41
+ let view = 'overview', lastUpdated = 0, failure = '', loading = true, announcementKind = '';
42
+ const text = (en, zh) => window.hccUi.language === 'zh' ? zh : en;
43
+ function apply() {
44
+ const labels = { overview: text('Overview', '概览'), tasks: text('Tasks', '任务'), messages: text('Messages', '消息'), events: text('Events', '事件') };
45
+ tabs.setAttribute('aria-label', text('Project views', '项目视图'));
46
+ tabs.querySelectorAll('[data-state-view]').forEach(button => {
47
+ const selected = button.dataset.stateView === view;
48
+ button.textContent = labels[button.dataset.stateView];
49
+ button.setAttribute('aria-selected', String(selected));
50
+ button.tabIndex = selected ? 0 : -1;
51
+ });
52
+ state.setAttribute('role', 'tabpanel');
53
+ state.setAttribute('aria-labelledby', 'stateTab-' + view);
54
+ state.querySelectorAll('.state-card[data-section]').forEach(card => { card.hidden = !groups[view].includes(card.dataset.section); });
55
+ notice.dataset.stale = String(Boolean(failure));
56
+ const time = lastUpdated ? new Date(lastUpdated).toLocaleTimeString(window.hccUi.language === 'zh' ? 'zh-CN' : 'en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
57
+ updated.textContent = failure
58
+ ? text('Update failed. ', '更新失败。') + (time ? text('Showing data from ', '当前数据更新于 ') + time : text('Data unavailable.', '暂时无法获取数据。'))
59
+ : time ? text('Updated ', '更新于 ') + time : text('Loading project state…', '正在加载项目状态…');
60
+ updated.title = failure;
61
+ const announcements = { loading: text('Loading project state', '正在加载项目状态'), loaded: text('Project state loaded', '项目状态已加载'), failed: lastUpdated ? text('Update failed. Previous data is retained.', '更新失败,保留上次成功的数据。') : text('Update failed. Data unavailable.', '更新失败,暂时无法获取数据。'), recovered: text('Project state updates recovered', '项目状态更新已恢复') };
62
+ const announcementText = announcements[announcementKind] || '';
63
+ if (announcement.textContent !== announcementText) announcement.textContent = announcementText;
64
+ retry.textContent = text('Retry', '重试');
65
+ retry.hidden = !failure;
66
+ retry.disabled = loading;
67
+ state.setAttribute('aria-busy', String(loading));
68
+ }
69
+ function select(next) {
70
+ if (!groups[next]) return;
71
+ view = next; state.scrollTop = 0; apply();
72
+ }
73
+ tabs.querySelectorAll('[data-state-view]').forEach(button => {
74
+ button.addEventListener('click', () => select(button.dataset.stateView));
75
+ button.addEventListener('keydown', event => {
76
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
77
+ event.preventDefault();
78
+ const buttons = [...tabs.querySelectorAll('[data-state-view]')];
79
+ const index = buttons.indexOf(button);
80
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? buttons.length - 1 : (index + (event.key === 'ArrowRight' ? 1 : -1) + buttons.length) % buttons.length;
81
+ select(buttons[next].dataset.stateView); buttons[next].focus();
82
+ });
83
+ });
84
+ window.hccWorkbench = {
85
+ apply,
86
+ fresh() { if (failure) announcementKind = 'recovered'; else if (!lastUpdated) announcementKind = 'loaded'; lastUpdated = Date.now(); failure = ''; loading = false; apply(); },
87
+ stale(error) { announcementKind = 'failed'; failure = error?.detail || error?.message || text('Request failed', '请求失败'); loading = false; apply(); },
88
+ loading() { loading = true; apply(); },
89
+ reset() { announcementKind = 'loading'; lastUpdated = 0; failure = ''; loading = true; state.textContent = ''; apply(); }
90
+ };
91
+ window.addEventListener('hcc:preferences', apply);
92
+ apply();
93
+ })();`;
94
+ }
@@ -0,0 +1,222 @@
1
+ export function normalizeWorkspaceLayout(value) {
2
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
3
+ const ratio = Number(source.ratio);
4
+ return { version: 1, open: source.open === true, direction: source.direction === 'rows' ? 'rows' : 'columns',
5
+ ratio: Number.isFinite(ratio) ? Math.max(.25, Math.min(.75, ratio)) : .5,
6
+ session: typeof source.session === 'string' && source.session.length <= 512 ? source.session : '' };
7
+ }
8
+
9
+ export function workspaceMessageValid(event, source, origin, project) {
10
+ const data = event.data;
11
+ return Boolean(source && event.source === source && event.origin === origin && data && typeof data === 'object'
12
+ && data.type === 'hcc-workspace' && data.version === 1 && data.project === project
13
+ && ['ready', 'selection', 'preferences', 'close'].includes(data.action)
14
+ && (data.session === undefined || typeof data.session === 'string' && data.session.length <= 512)
15
+ && (data.preferences === undefined || data.preferences && typeof data.preferences === 'object' && !Array.isArray(data.preferences)));
16
+ }
17
+
18
+ export const workspaceCss = `
19
+ .workspace { min-width:0; min-height:0; display:flex; flex-direction:column; overflow:hidden; }
20
+ .workspace-panes { flex:1; min-height:0; min-width:0; display:grid; grid-template-columns:minmax(0,1fr); }
21
+ .workspace-bar { flex:none; display:flex; align-items:center; flex-wrap:wrap; gap:8px; padding:8px 10px; background:var(--panel); border-bottom:1px solid var(--border); }
22
+ .workspace-bar[hidden], .workspace-secondary[hidden], .workspace-divider[hidden] { display:none; }
23
+ .workspace-bar label { display:flex; flex:1; align-items:center; gap:8px; min-width:160px; }
24
+ .workspace-bar select { min-width:0; flex:1; max-width:350px; }
25
+ .workspace-pane-tabs { display:none; gap:6px; }
26
+ .workspace-bar button[aria-pressed="true"] { background:var(--selection-bg); border-color:var(--accent); }
27
+ .workspace-secondary { display:flex; position:relative; flex-direction:column; min-width:0; min-height:0; overflow:hidden; background:var(--bg); }
28
+ .workspace-secondary iframe { width:100%; flex:1; min-height:0; border:0; background:var(--bg); }
29
+ .workspace-status { padding:12px; color:var(--muted); font-size:var(--small-font); }
30
+ .workspace-status[hidden] { display:none; }
31
+ .workspace-divider { width:8px; height:100%; border:0; padding:0; cursor:col-resize; border-radius:0; background:var(--border); touch-action:none; }
32
+ .workspace-divider:focus-visible { outline-offset:-2px; }
33
+ .workspace-panes[data-direction="columns"] { grid-template-columns:minmax(0,var(--workspace-ratio,1fr)) 8px minmax(0,var(--workspace-rest,1fr)); }
34
+ .workspace-panes[data-direction="rows"] { grid-template-columns:minmax(0,1fr); grid-template-rows:minmax(420px,var(--workspace-ratio,1fr)) 8px minmax(420px,var(--workspace-rest,1fr)); overflow:auto; }
35
+ .workspace-panes[data-direction="rows"] .workspace-divider { width:100%; height:8px; cursor:row-resize; }
36
+ .workspace-panes.resizing iframe { pointer-events:none; }
37
+ .app.workspace-open { grid-template-columns:var(--left-width) minmax(0,1fr) 0; }
38
+ .app.workspace-open.left-collapsed, .app.workspace-open.focus-mode { grid-template-columns:0 minmax(0,1fr) 0; }
39
+ .app.workspace-open .inspector { display:none; }
40
+ .app.workspace-open .edge-right, .app.workspace-open .edge-resizer-right { display:none; }
41
+ .workspace-open .main > .toolbar { flex-wrap:wrap; }
42
+ .workspace-open .main > .toolbar .title { flex-basis:100%; }
43
+ .workspace-open .quick, .session-pane .quick { flex:1 1 auto; flex-wrap:wrap; min-width:0; }
44
+ @media(max-width:1099px) {
45
+ .app .workspace { display:none; }
46
+ .app[data-view="terminal"] .workspace { display:flex; }
47
+ .app.workspace-open, .app.workspace-open.left-collapsed, .app.workspace-open.focus-mode { grid-template-columns:minmax(0,1fr); }
48
+ .app.workspace-open[data-view="state"] .inspector { display:grid; visibility:visible; }
49
+ .workspace-pane-tabs { display:flex; }
50
+ .workspace-panes[data-direction] { grid-template-columns:minmax(0,1fr); grid-template-rows:minmax(0,1fr); }
51
+ .workspace-panes .workspace-divider, #workspaceDirection { display:none; }
52
+ .app[data-view="terminal"] .workspace[data-active-pane="secondary"] .main { display:none; }
53
+ .workspace[data-active-pane="primary"] .workspace-secondary { display:none; }
54
+ .workspace-bar { gap:6px; }
55
+ .workspace-bar label span { display:none; }
56
+ .workspace-bar label { min-width:100px; }
57
+ }
58
+ .session-pane .global-header, .session-pane .mobile-nav, .session-pane .sidebar,
59
+ .session-pane .inspector, .session-pane .edge-toggle, .session-pane .edge-resizer,
60
+ .session-pane #workspaceBar { display:none !important; }
61
+ .session-pane .app { display:flex; height:100dvh; }
62
+ .session-pane .app .workspace, .session-pane .app .main { display:flex; flex:1; visibility:visible; }
63
+ .session-pane .toolbar { flex-wrap:wrap; }
64
+ .session-pane .toolbar .title { flex-basis:100%; }
65
+ @media(pointer:coarse) { .workspace-bar button, .workspace-bar select { min-height:44px; } }
66
+ `;
67
+
68
+ export function workspaceBarHtml() {
69
+ return `<header class="workspace-bar" id="workspaceBar" hidden>
70
+ <div class="workspace-pane-tabs" role="group" data-i18n-aria="workspace.panes" aria-label="Session panes">
71
+ <button id="workspacePrimary" type="button" data-i18n="workspace.primary" aria-pressed="true">Main</button>
72
+ <button id="workspaceSecondary" type="button" data-i18n="workspace.secondary" aria-pressed="false">Second</button>
73
+ </div>
74
+ <label><span data-i18n="workspace.secondSession">Second session</span><select id="workspaceSession" data-i18n-aria="workspace.secondSession" aria-label="Second session"></select></label>
75
+ <button id="workspaceDirection" type="button">Stack vertically</button>
76
+ <button id="workspaceClose" type="button" data-i18n="workspace.close" data-i18n-title="workspace.closeHelp" title="Close this pane; the session keeps running">Close split</button>
77
+ </header>`;
78
+ }
79
+
80
+ export function workspaceSecondaryHtml() {
81
+ return `<div class="workspace-divider" id="workspaceDivider" role="separator" tabindex="0" aria-orientation="vertical" aria-valuemin="25" aria-valuemax="75" aria-valuenow="50" data-i18n-aria="workspace.resize" aria-label="Resize session panes" hidden></div>
82
+ <section class="workspace-secondary" id="workspaceSecondPane" data-i18n-aria="workspace.secondary" aria-label="Second pane" hidden>
83
+ <div class="workspace-status" id="workspaceStatus" role="status"></div>
84
+ <button id="workspaceRetry" type="button" data-i18n="workspace.retry" hidden>Reconnect pane</button>
85
+ </section>`;
86
+ }
87
+
88
+ function workspaceRuntime(normalize, validMessage) {
89
+ const host = window.hccWorkspaceHost, ui = window.hccUi;
90
+ const tr = key => ui.tr(key);
91
+ const byId = id => document.getElementById(id);
92
+ const workspace = byId('workspace'), panes = byId('workspacePanes'), bar = byId('workspaceBar');
93
+ const secondary = byId('workspaceSecondPane'), status = byId('workspaceStatus'), select = byId('workspaceSession');
94
+ const divider = byId('workspaceDivider'), split = byId('splitBtn');
95
+ const mobile = window.matchMedia('(max-width:1099px)');
96
+ let root = '', layout = normalize(null), frame = null, mounted = '', timer = 0, selectedPane = 'primary', resizing = false, applyingParentPreferences = false;
97
+ const key = () => 'hcc.workspace:' + root;
98
+ const save = () => { if (root) ui.safeSet(key(), JSON.stringify(layout)); };
99
+ const post = action => {
100
+ const data = {type:'hcc-workspace', version:1, action, project:host.project(), session:host.active() || '', preferences:ui.preferences};
101
+ if (host.embedded) { if (window.parent !== window) window.parent.postMessage(data, location.origin); }
102
+ else frame?.contentWindow?.postMessage({...data, session:layout.session}, location.origin);
103
+ };
104
+ function destroy() {
105
+ clearTimeout(timer); mounted = ''; frame?.remove(); frame = null;
106
+ status.hidden = true; byId('workspaceRetry').hidden = true;
107
+ }
108
+ function appearance() {
109
+ bar.hidden = !layout.open; secondary.hidden = divider.hidden = !layout.open;
110
+ host.app.classList.toggle('workspace-open', layout.open);
111
+ split.setAttribute('aria-pressed', String(layout.open));
112
+ if (layout.open) panes.dataset.direction = layout.direction; else delete panes.dataset.direction;
113
+ panes.style.setProperty('--workspace-ratio', layout.ratio + 'fr');
114
+ panes.style.setProperty('--workspace-rest', (1-layout.ratio) + 'fr');
115
+ divider.setAttribute('aria-orientation', layout.direction === 'columns' ? 'vertical' : 'horizontal');
116
+ divider.setAttribute('aria-valuenow', String(Math.round(layout.ratio*100)));
117
+ byId('workspaceDirection').textContent = tr(layout.direction === 'columns' ? 'workspace.rows' : 'workspace.columns');
118
+ workspace.dataset.activePane = selectedPane;
119
+ byId('workspacePrimary').setAttribute('aria-pressed', String(selectedPane === 'primary'));
120
+ byId('workspaceSecondary').setAttribute('aria-pressed', String(selectedPane === 'secondary'));
121
+ secondary.inert = mobile.matches && (selectedPane !== 'secondary' || host.app.dataset.view !== 'terminal');
122
+ host.primary.inert = mobile.matches && (layout.open && selectedPane !== 'primary' || host.app.dataset.view !== 'terminal');
123
+ host.layoutChanged();
124
+ }
125
+ function mount() {
126
+ if (!layout.open || !host.ready()) return;
127
+ const session = host.sessions().find(item => item.id === layout.session && item.status === 'running');
128
+ if (!session) {
129
+ destroy(); status.hidden = false; status.textContent = tr('workspace.missing'); return;
130
+ }
131
+ const identity = JSON.stringify([root,session.id]);
132
+ if (frame && mounted === identity) return;
133
+ destroy(); mounted = identity; status.hidden = false; status.textContent = tr('workspace.connecting');
134
+ frame = document.createElement('iframe'); frame.id = 'workspaceFrame'; frame.title = tr('workspace.secondary');
135
+ const url = new URL('/pane', location.origin); url.searchParams.set('project',root); url.searchParams.set('session',session.id);
136
+ frame.src = url.href; secondary.appendChild(frame);
137
+ timer = setTimeout(() => { status.hidden = false; status.textContent = tr('workspace.failed'); byId('workspaceRetry').hidden = false; }, 12000);
138
+ }
139
+ function sync() {
140
+ if (host.embedded) { post('selection'); return; }
141
+ const nextRoot = host.project();
142
+ if (nextRoot !== root) {
143
+ destroy(); root = nextRoot;
144
+ try { layout = normalize(JSON.parse(ui.safeGet(key()) || 'null')); } catch (_) { layout = normalize(null); }
145
+ selectedPane = 'primary';
146
+ }
147
+ split.disabled = !host.ready() || !host.sessions().some(item => item.status === 'running');
148
+ const options = host.sessions().filter(item => item.status === 'running');
149
+ const esc = host.esc;
150
+ const html = options.map(item => '<option value="'+esc(item.id)+'">'+esc(item.name || item.task?.title || item.peer_id || item.id)+'</option>').join('');
151
+ const missing = layout.session && !options.some(item => item.id === layout.session);
152
+ const nextHtml = (missing ? '<option value="'+esc(layout.session)+'" disabled>'+esc(layout.session)+' · '+esc(tr('workspace.missingShort'))+'</option>' : '') + html;
153
+ if (select.innerHTML !== nextHtml) select.innerHTML = nextHtml;
154
+ select.value = layout.session;
155
+ appearance(); if (layout.open) mount();
156
+ }
157
+ function open() {
158
+ if (host.embedded || split.disabled) return;
159
+ if (!layout.session) layout.session = (host.sessions().find(item => item.status === 'running' && item.id !== host.active()) || host.sessions().find(item => item.status === 'running'))?.id || '';
160
+ layout.open = true; selectedPane = mobile.matches ? 'secondary' : 'primary'; save(); host.showTerminal(); sync();
161
+ }
162
+ function close() { layout.open = false; destroy(); selectedPane = 'primary'; save(); appearance(); split.focus(); }
163
+ function reset() { destroy(); root = ''; layout = normalize(null); appearance(); }
164
+ function pickPane(value) { selectedPane = value; host.showTerminal(); appearance(); }
165
+ function resize(event) {
166
+ if (!resizing) return;
167
+ const rect = panes.getBoundingClientRect();
168
+ const fraction = layout.direction === 'columns' ? (event.clientX-rect.left)/rect.width : (event.clientY-rect.top)/rect.height;
169
+ layout.ratio = Math.max(.25,Math.min(.75,fraction)); appearance();
170
+ }
171
+ if (!host.embedded) {
172
+ split.addEventListener('click', () => layout.open ? close() : open());
173
+ byId('workspaceClose').addEventListener('click', close);
174
+ select.addEventListener('change', () => {
175
+ if (!host.sessions().some(item => item.id === select.value && item.status === 'running')) return;
176
+ layout.session = select.value; selectedPane = 'secondary'; save(); mount(); appearance();
177
+ });
178
+ byId('workspaceDirection').addEventListener('click', () => { layout.direction = layout.direction === 'columns' ? 'rows' : 'columns'; save(); appearance(); });
179
+ byId('workspacePrimary').addEventListener('click', () => pickPane('primary'));
180
+ byId('workspaceSecondary').addEventListener('click', () => pickPane('secondary'));
181
+ byId('workspaceRetry').addEventListener('click', () => { destroy(); mount(); });
182
+ divider.addEventListener('pointerdown', event => { if (event.button !== 0) return; event.preventDefault(); resizing = true; panes.classList.add('resizing'); divider.setPointerCapture(event.pointerId); });
183
+ divider.addEventListener('pointermove', resize);
184
+ for (const name of ['pointerup','pointercancel','lostpointercapture']) divider.addEventListener(name, () => { resizing = false; panes.classList.remove('resizing'); save(); });
185
+ divider.addEventListener('keydown', event => {
186
+ const decrease = layout.direction === 'columns' ? 'ArrowLeft' : 'ArrowUp', increase = layout.direction === 'columns' ? 'ArrowRight' : 'ArrowDown';
187
+ if (![decrease,increase,'Home','End'].includes(event.key)) return;
188
+ event.preventDefault(); layout.ratio = event.key === 'Home' ? .25 : event.key === 'End' ? .75 : Math.max(.25,Math.min(.75,layout.ratio+(event.key === increase ? .05 : -.05)));
189
+ save(); appearance();
190
+ });
191
+ mobile.addEventListener('change', appearance);
192
+ }
193
+ window.addEventListener('message', event => {
194
+ const source = host.embedded ? window.parent : frame?.contentWindow;
195
+ if (!validMessage(event, source, location.origin, host.project())) return;
196
+ const data = event.data;
197
+ if (host.embedded) {
198
+ if (data.action === 'preferences') {
199
+ applyingParentPreferences = true;
200
+ try { ui.update(data.preferences); } finally { applyingParentPreferences = false; }
201
+ }
202
+ return;
203
+ }
204
+ if (!layout.open) return;
205
+ if (data.action === 'ready') post('preferences');
206
+ if (data.action === 'selection') {
207
+ if (data.session && host.sessions().some(item => item.id === data.session && item.status === 'running')) {
208
+ clearTimeout(timer); status.hidden = true; byId('workspaceRetry').hidden = true;
209
+ layout.session = data.session; mounted = JSON.stringify([root,data.session]); select.value = data.session; save();
210
+ }
211
+ }
212
+ if (data.action === 'preferences') ui.update(data.preferences);
213
+ });
214
+ window.addEventListener('hcc:preferences', () => { if (!host.embedded) { sync(); if (frame) frame.title = tr('workspace.secondary'); } if (!applyingParentPreferences) post('preferences'); });
215
+ window.addEventListener('pagehide', destroy);
216
+ window.hccWorkspace = {open, close, sync, reset, access:appearance, destroy, primary:() => pickPane('primary'), get isOpen() { return layout.open; }};
217
+ if (host.embedded) { post('ready'); } else sync();
218
+ }
219
+
220
+ export function workspaceScript() {
221
+ return '(' + workspaceRuntime.toString() + ')(' + normalizeWorkspaceLayout.toString() + ',' + workspaceMessageValid.toString() + ');';
222
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logicseek/hello-cc",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Local control plane for coordinating Claude Code, Codex, and other coding CLI sessions.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,8 @@
19
19
  "docs/"
20
20
  ],
21
21
  "engines": {
22
- "node": ">=24.0.0"
22
+ "node": ">=24.0.0",
23
+ "dsh": "0.2.0-rc.2"
23
24
  },
24
25
  "scripts": {
25
26
  "release:notes": "node ./scripts/release-notes.mjs",
@@ -55,9 +56,16 @@
55
56
  ],
56
57
  "dependencies": {
57
58
  "@xterm/addon-fit": "^0.11.0",
59
+ "@xterm/addon-search": "0.16.0",
58
60
  "@xterm/xterm": "^6.0.0",
59
61
  "node-pty": "1.2.0-beta.15",
60
62
  "ws": "^8.21.0"
61
63
  },
62
- "license": "Apache-2.0"
64
+ "license": "Apache-2.0",
65
+ "dsh": {
66
+ "manifestVersion": 1,
67
+ "bundle": {
68
+ "patch": "./lib/integrations/dsh.bundle.yml"
69
+ }
70
+ }
63
71
  }
@@ -119,6 +119,37 @@ function stripCommentsAndStrings(source) {
119
119
  function collectBindings(stripped, factoryStart) {
120
120
  const bound = new Set();
121
121
  const body = stripped;
122
+ // Split variable declarations only at top-level commas: initializer object
123
+ // keys, calls and array values must never be mistaken for declared names.
124
+ for (const m of body.matchAll(/\b(?:const|let|var)\s+/g)) {
125
+ let start = m.index + m[0].length;
126
+ let depth = 0;
127
+ for (let i = start; i < body.length; i += 1) {
128
+ const ch = body[i];
129
+ if ('([{'.includes(ch)) depth += 1;
130
+ if (')]}'.includes(ch)) {
131
+ if (depth === 0) break;
132
+ depth -= 1;
133
+ }
134
+ if (depth === 0 && (ch === ',' || ch === ';')) {
135
+ for (const leaf of parameterBindings(body.slice(start, i).split('=')[0])) bound.add(leaf);
136
+ start = i + 1;
137
+ if (ch === ';') break;
138
+ }
139
+ if (depth === 0 && /^\s+(?:of|in)\s/.test(body.slice(i))) {
140
+ for (const leaf of parameterBindings(body.slice(start, i))) bound.add(leaf);
141
+ break;
142
+ }
143
+ }
144
+ }
145
+ for (const m of body.matchAll(/\bfunction\s+[A-Za-z_$][\w$]*\s*\(/g)) {
146
+ const parameters = balancedContents(body, m.index + m[0].length - 1, '(', ')');
147
+ for (const leaf of parameterBindings(parameters)) bound.add(leaf);
148
+ }
149
+ for (const m of body.matchAll(/\b(?:const|let|var)\s*\{/g)) {
150
+ const declaration = balancedContents(body, m.index + m[0].length - 1, '{', '}');
151
+ for (const leaf of parameterBindings(declaration)) bound.add(leaf);
152
+ }
122
153
 
123
154
  // imports (whole file)
124
155
  for (const m of body.matchAll(/import\s+(?:\*\s+as\s+)?([A-Za-z_$][\w$]*)/g)) bound.add(m[1]);
@@ -141,10 +172,17 @@ function collectBindings(stripped, factoryStart) {
141
172
  // function declarations + their parameters
142
173
  for (const m of body.matchAll(/(?:^|\n)\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)?\s*\(([^)]*)\)/g)) {
143
174
  if (m[1]) bound.add(m[1]);
144
- for (const p of m[2].split(',')) {
145
- const name = p.trim().split(/[=\s]/)[0].replace(/^\.\.\./, '');
146
- for (const leaf of destructLeaves(name)) bound.add(leaf);
147
- }
175
+ for (const leaf of parameterBindings(m[2])) bound.add(leaf);
176
+ }
177
+ // Object/class methods and destructured options are ordinary local bindings.
178
+ // Keep this scanner conservative, like its file-wide declaration collection.
179
+ for (const m of body.matchAll(/(?:^|[\n,{])\s*(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(/g)) {
180
+ if (['if', 'while', 'switch', 'catch', 'for'].includes(m[1])) continue;
181
+ const start = m.index + m[0].length - 1;
182
+ const parameters = balancedContents(body, start, '(', ')');
183
+ if (!/^\s*\{/.test(body.slice(start + parameters.length + 2))) continue;
184
+ bound.add(m[1]);
185
+ for (const leaf of parameterBindings(parameters)) bound.add(leaf);
148
186
  }
149
187
  // const/let/var with destructuring or plain names
150
188
  for (const m of body.matchAll(/(?:^|[^.\w$])(?:const|let|var)\s+([A-Za-z_$][\w$]*|\{[^}]*\})\s*[=;]/g)) {
@@ -161,9 +199,7 @@ function collectBindings(stripped, factoryStart) {
161
199
  }
162
200
  // arrow function parameters: (a, b) => / single a =>
163
201
  for (const m of body.matchAll(/\(\s*([^()]*?)\s*\)\s*=>/g)) {
164
- for (const p of m[1].split(',')) {
165
- for (const leaf of destructLeaves(p.trim().split(/[=\s]/)[0].replace(/^\.\.\./, ''))) bound.add(leaf);
166
- }
202
+ for (const leaf of parameterBindings(m[1])) bound.add(leaf);
167
203
  }
168
204
  for (const m of body.matchAll(/(?:^|[^.\w$])([A-Za-z_$][\w$]*)\s*=>/g)) bound.add(m[1]);
169
205
  // object literal shorthand keys are uses of bindings, but `{ name }` in a
@@ -186,8 +222,29 @@ function destructLeaves(fragment) {
186
222
  return leaves;
187
223
  }
188
224
 
225
+ function parameterBindings(parameters) {
226
+ const result = [];
227
+ // Default expressions are not bindings; after literal stripping their
228
+ // identifiers still belong to existing imports or declarations.
229
+ for (const piece of parameters.replace(/[{}\[\]]/g, '').split(',')) {
230
+ const declaration = piece.split('=')[0].trim();
231
+ const name = declaration.split(':').pop()?.trim().replace(/^\.\.\./, '');
232
+ if (name && /^[A-Za-z_$][\w$]*$/.test(name)) result.push(name);
233
+ }
234
+ return result;
235
+ }
236
+
237
+ function balancedContents(source, start, open, close) {
238
+ let depth = 0;
239
+ for (let i = start; i < source.length; i += 1) {
240
+ if (source[i] === open) depth += 1;
241
+ if (source[i] === close && --depth === 0) return source.slice(start + 1, i);
242
+ }
243
+ return '';
244
+ }
245
+
189
246
  const JS_GLOBALS = new Set(('globalThis global console process require module exports Buffer URL URLSearchParams'
190
- + ' Array Object String Number Boolean Math JSON Date Promise Symbol BigInt Map Set WeakMap WeakSet'
247
+ + ' Array Object String Number Boolean Math JSON Date Promise Symbol BigInt Map Set WeakMap WeakSet Proxy Reflect'
191
248
  + ' RegExp Error TypeError RangeError SyntaxError ReferenceError EvalError URIError AggregateError'
192
249
  + ' parseInt parseFloat isNaN isFinite encodeURIComponent decodeURIComponent'
193
250
  + ' undefined NaN Infinity setTimeout setInterval clearTimeout setImmediate clearImmediate queueMicrotask'
@@ -244,6 +301,8 @@ const KNOWN_FALSE_POSITIVES = {
244
301
  'lib/cli/commands/doctor.mjs': ['connectReadOnly', 'k', 'v', 'wal'],
245
302
  'lib/cli/commands/gc.mjs': ['add', 'apply', 'dry', 'entries', 'gcCutoff', 'id', 'lock', 'run', 'to', 'yes'],
246
303
  'lib/cli/commands/install.mjs': ['cc', 'n'],
304
+ // Literal query key in a nested template interpolation; opts.peer is bound.
305
+ 'lib/cli/commands/native.mjs': ['peer'],
247
306
  'lib/cli/commands/peer.mjs': ['pid'],
248
307
  'lib/cli/commands/task.mjs': ['child'],
249
308
  'lib/cli/commands/team.mjs': ['assignee', 'count', 'owner', 'status'],
@@ -0,0 +1,72 @@
1
+ // Read-only installed CLI diagnostics in a disposable project/home. No model,
2
+ // account configuration, existing session, or user project is used.
3
+ import assert from 'node:assert/strict';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { createHash } from 'node:crypto';
8
+ import { spawnSync } from 'node:child_process';
9
+ import { DatabaseSync } from 'node:sqlite';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { initSchema } from '../lib/db/schema.mjs';
12
+
13
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'hcc-installed-diagnostics-'));
14
+ const root = path.join(directory, 'project');
15
+ const home = path.join(directory, 'home');
16
+ const codexHome = path.join(directory, 'codex-home');
17
+ const state = path.join(root, '.hello-cc');
18
+ for (const entry of [state, home, codexHome]) fs.mkdirSync(entry, { recursive: true });
19
+ const db = new DatabaseSync(path.join(state, 'mesh.db'));
20
+ try { initSchema(db); } finally { db.close(); }
21
+ fs.writeFileSync(path.join(codexHome, 'hooks.json'), '{}\n');
22
+ const cli = fileURLToPath(new URL('../bin/hcc.mjs', import.meta.url));
23
+ const env = { PATH: process.env.PATH, HOME: home, CODEX_HOME: codexHome,
24
+ HCC_SHIM_NO_ATTACH: '1', HCC_SHIM_ENSURED: '1' };
25
+
26
+ function files(parent = directory) {
27
+ return fs.readdirSync(parent, { withFileTypes: true }).flatMap(entry => {
28
+ const file = path.join(parent, entry.name);
29
+ return entry.isDirectory() ? files(file) : [[path.relative(directory, file),
30
+ createHash('sha256').update(fs.readFileSync(file)).digest('hex')]];
31
+ }).sort(([left], [right]) => left.localeCompare(right));
32
+ }
33
+
34
+ function doctor(args = []) {
35
+ const result = spawnSync(process.execPath, [cli, '--root', root, '--json', 'doctor', ...args], {
36
+ cwd: root, env, encoding: 'utf8', timeout: 15000, maxBuffer: 128 * 1024,
37
+ stdio: ['ignore', 'pipe', 'pipe']
38
+ });
39
+ // Do not expose arbitrary executable/configuration output on failure.
40
+ assert.equal(result.error, undefined, 'Isolated doctor process failed');
41
+ assert.equal(result.status, 0, 'Isolated doctor rejected the healthy database');
42
+ const response = JSON.parse(result.stdout);
43
+ assert.equal(response.ok, true, 'Isolated doctor did not return a successful report');
44
+ return response.data;
45
+ }
46
+
47
+ let evidence;
48
+ try {
49
+ const before = files();
50
+ const plain = doctor();
51
+ assert.equal(Object.hasOwn(plain, 'codex'), false, 'Default doctor unexpectedly probes Codex');
52
+ const report = doctor(['--codex']);
53
+ assert.equal(report.codex.installed.status, 'present');
54
+ assert.equal(report.codex.version.status, 'known');
55
+ assert.equal(report.codex.app_server.status, 'advertised');
56
+ assert.ok(report.codex.app_server.startup_arguments?.length);
57
+ assert.equal(report.codex.app_server.protocol_handshake, 'unknown');
58
+ assert.equal(report.codex.hooks.configuration.status, 'missing');
59
+ assert.equal(report.codex.hooks.invocation.status, 'unknown');
60
+ assert.equal(report.codex.hooks.stdout_delivery_receipt.status, 'unknown');
61
+ assert.equal(report.codex.hooks.provider_acceptance, 'unknown');
62
+ assert.equal(report.codex.hooks.trust, 'unknown');
63
+ assert.deepEqual(files(), before, 'Diagnostics changed the project, home, or hook configuration');
64
+ evidence = { installedVersion: report.codex.version.value,
65
+ appServerStartupArguments: report.codex.app_server.startup_arguments,
66
+ defaultDoctorDatabaseOnly: true, projectAndHomeUnchanged: true,
67
+ configurationDoesNotProveDeliveryOrTrust: true, inferenceCalled: false };
68
+ } finally {
69
+ fs.rmSync(directory, { recursive: true, force: true });
70
+ }
71
+ console.log(JSON.stringify({ ...evidence, temporaryDirectoriesRemoved: true }));
72
+ console.log('INSTALLED_CODEX_DIAGNOSTICS_OK');