@logicseek/hello-cc 0.1.8 → 1.0.0

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 (108) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +26 -4
  3. package/README.zh-CN.md +22 -3
  4. package/bin/hcc.mjs +3 -5622
  5. package/docs/README.md +8 -0
  6. package/docs/README.zh-CN.md +6 -0
  7. package/docs/architecture.md +29 -18
  8. package/docs/architecture.zh-CN.md +20 -10
  9. package/docs/commands.md +16 -5
  10. package/docs/commands.zh-CN.md +16 -5
  11. package/docs/current-state-review-2026-08.md +341 -0
  12. package/docs/defect-review-2026-07.md +382 -0
  13. package/docs/guide.md +53 -5
  14. package/docs/guide.zh-CN.md +46 -3
  15. package/docs/superpowers/plans/2026-08-03-runtime-safety-plan.md +294 -0
  16. package/docs/superpowers/plans/2026-08-03-v1-data-identity-plan.md +400 -0
  17. package/docs/superpowers/plans/2026-08-03-v1-release-verification-plan.md +284 -0
  18. package/docs/superpowers/plans/2026-08-03-web-api-hardening-plan.md +303 -0
  19. package/docs/superpowers/plans/2026-08-04-runtime-buffer-gc-two-phase.md +71 -0
  20. package/docs/superpowers/specs/2026-08-03-runtime-safety-design.md +117 -0
  21. package/docs/superpowers/specs/2026-08-03-v1-data-identity-design.md +120 -0
  22. package/docs/superpowers/specs/2026-08-03-v1-release-verification-design.md +117 -0
  23. package/docs/superpowers/specs/2026-08-03-web-api-hardening-design.md +104 -0
  24. package/lib/cli/app.mjs +642 -0
  25. package/lib/cli/commands/coordination.mjs +247 -0
  26. package/lib/cli/commands/doctor.mjs +81 -0
  27. package/lib/cli/commands/gc.mjs +852 -0
  28. package/lib/cli/commands/hook.mjs +205 -0
  29. package/lib/cli/commands/install.mjs +294 -0
  30. package/lib/cli/commands/lifecycle.mjs +101 -0
  31. package/lib/cli/commands/lock.mjs +197 -0
  32. package/lib/cli/commands/misc.mjs +61 -0
  33. package/lib/cli/commands/msg.mjs +152 -0
  34. package/lib/cli/commands/peer.mjs +233 -0
  35. package/lib/cli/commands/query.mjs +96 -0
  36. package/lib/cli/commands/run.mjs +463 -0
  37. package/lib/cli/commands/task.mjs +405 -0
  38. package/lib/cli/commands/team.mjs +131 -0
  39. package/lib/cli/commands/tmux.mjs +838 -0
  40. package/lib/cli/commands/up.mjs +135 -0
  41. package/lib/cli-args.mjs +16 -2
  42. package/lib/coordination-state.mjs +112 -51
  43. package/lib/core/coordination/automation.mjs +5 -8
  44. package/lib/core/coordination/clock-safety.mjs +323 -0
  45. package/lib/core/coordination/event-retention.mjs +17 -0
  46. package/lib/core/coordination/gc-plan.mjs +614 -0
  47. package/lib/core/coordination/lease-renewal.mjs +102 -0
  48. package/lib/core/coordination/lock-evidence.mjs +77 -0
  49. package/lib/core/coordination/optimistic-evidence.mjs +26 -0
  50. package/lib/core/coordination/tasks.mjs +156 -36
  51. package/lib/core/peers/evidence-runtime.mjs +312 -0
  52. package/lib/core/peers/evidence.mjs +103 -0
  53. package/lib/core/peers/hook-owner.mjs +32 -0
  54. package/lib/core/peers/liveness.mjs +44 -4
  55. package/lib/core/peers/peer-helpers.mjs +121 -0
  56. package/lib/core/peers/session.mjs +8 -2
  57. package/lib/core/peers/tmux-safety.mjs +290 -0
  58. package/lib/core/sessions/launch.mjs +15 -1
  59. package/lib/db/connection.mjs +104 -0
  60. package/lib/db/events.mjs +34 -0
  61. package/lib/db/migration-backup.mjs +225 -0
  62. package/lib/db/schema.mjs +102 -5
  63. package/lib/db/stores/peers.mjs +34 -0
  64. package/lib/integrations/peers/identity.mjs +62 -16
  65. package/lib/integrations/shims/script.mjs +139 -15
  66. package/lib/process/identity.mjs +192 -0
  67. package/lib/process/pty-lifecycle.mjs +149 -0
  68. package/lib/runtime/buffer-directory-lease.mjs +47 -0
  69. package/lib/runtime/buffer-evidence.mjs +336 -0
  70. package/lib/runtime/buffer-gc-protocol.mjs +167 -0
  71. package/lib/runtime/buffer-gc.mjs +270 -0
  72. package/lib/runtime/client.mjs +61 -8
  73. package/lib/runtime/fatal-shutdown.mjs +54 -0
  74. package/lib/runtime/project-path.mjs +139 -0
  75. package/lib/runtime/projects.mjs +320 -40
  76. package/lib/runtime/state.mjs +232 -29
  77. package/lib/shared/cleanup-error.mjs +54 -0
  78. package/lib/shared/clock-grace.mjs +106 -0
  79. package/lib/shared/errors.mjs +9 -0
  80. package/lib/shared/file-lock.mjs +370 -0
  81. package/lib/shared/json-file.mjs +6 -2
  82. package/lib/shared/redact.mjs +60 -0
  83. package/lib/shared/socket-lock-worker.mjs +201 -0
  84. package/lib/terminal/tmux.mjs +78 -4
  85. package/lib/tmux.mjs +4 -0
  86. package/lib/ui/help.mjs +53 -15
  87. package/lib/web/api-version.mjs +42 -0
  88. package/lib/web/auto-attach.mjs +185 -0
  89. package/lib/web/buffer-gc-runtime.mjs +175 -0
  90. package/lib/web/cookie-auth.mjs +119 -0
  91. package/lib/web/external-sessions.mjs +254 -0
  92. package/lib/web/http-routes.mjs +542 -0
  93. package/lib/web/http.mjs +143 -7
  94. package/lib/web/liveness-reaper.mjs +145 -0
  95. package/lib/web/peer-actions.mjs +132 -23
  96. package/lib/web/project-contexts.mjs +178 -0
  97. package/lib/web/pty-sessions.mjs +203 -0
  98. package/lib/web/runtime-main.mjs +714 -0
  99. package/lib/web/runtime.mjs +91 -36
  100. package/lib/web/session-serialize.mjs +0 -0
  101. package/lib/web/startup.mjs +333 -0
  102. package/lib/web/tls.mjs +365 -0
  103. package/lib/web/tmux-sessions.mjs +1361 -0
  104. package/lib/web/tmux-stream.mjs +166 -0
  105. package/lib/web/ui-template.mjs +152 -20
  106. package/package.json +5 -2
  107. package/scripts/check-free-identifiers.mjs +290 -0
  108. package/scripts/regression.mjs +5646 -262
@@ -0,0 +1,178 @@
1
+ // Web project context management, extracted from lib/web/runtime-main.mjs.
2
+ // Owns the projectContexts map: normalization, registry-backed discovery,
3
+ // fail-closed path re-resolution, per-project DB connections, and
4
+ // session/peer lookup helpers used by the HTTP and WS layers.
5
+
6
+ import path from 'node:path';
7
+ import { randomBytes } from 'node:crypto';
8
+ import { CliError } from '../shared/errors.mjs';
9
+ import { contextForProject } from '../runtime/paths.mjs';
10
+ import { resolveProjectDatabase } from '../runtime/project-path.mjs';
11
+ import {
12
+ projectRecord,
13
+ readProjectRegistry,
14
+ registerProject,
15
+ registerProjectActivity
16
+ } from '../runtime/projects.mjs';
17
+ import { nextSessionId } from '../web/runtime.mjs';
18
+ import { tokenMatches } from '../web/http.mjs';
19
+
20
+ export function createProjectContexts(deps) {
21
+ const {
22
+ ctx, sessions,
23
+ sessionKey, sessionsForProject, resolveSessionPeerId,
24
+ connect, now, addEvent, tx, touchPeer, upsertPeer, detectBranch,
25
+ ACTIVE_PEER_TTL, CLI_NAME, DEFAULT_LOCK_TTL,
26
+ queryInbox, queryOpenTasks, queryTimelineMessages,
27
+ observePeerEvidence, peerEvidenceFromDb,
28
+ observeClockSafetyInTransaction,
29
+ assertTaskOwnerForMutation, claimNextTasksForPeer, takeOverTaskForPeer,
30
+ positiveIntOpt, sameResolvedPath
31
+ } = deps;
32
+
33
+ const projectContexts = new Map();
34
+
35
+ function newSessionActionToken() {
36
+ return randomBytes(32).toString('base64url');
37
+ }
38
+
39
+ function rememberProject(projectCtx, { activity = false, register = false, nonblocking = false } = {}) {
40
+ const normalized = contextForProject(projectCtx.root, projectCtx.dbPath, { cwd: projectCtx.cwd, json: ctx.json });
41
+ const isNew = !projectContexts.has(normalized.root);
42
+ // Activity refresh is already nonblocking and throttled before it tries the
43
+ // registry lock. Calling it on each request keeps last_seen_at current
44
+ // without putting the synchronous lock back on the HTTP hot path.
45
+ if (activity) registerProjectActivity(normalized);
46
+ else if (isNew || register) {
47
+ try {
48
+ registerProject(normalized, { nonblocking });
49
+ } catch (error) {
50
+ if (nonblocking && ['ERR_FILE_LOCK_BUSY', 'ERR_FILE_LOCK_TIMEOUT'].includes(error?.code)) {
51
+ throw new CliError('REGISTRY_BUSY', 'Project registry is busy; retry the request');
52
+ }
53
+ throw error;
54
+ }
55
+ }
56
+ projectContexts.set(normalized.root, normalized);
57
+ return normalized;
58
+ }
59
+
60
+ function knownProjects() {
61
+ const rows = readProjectRegistry();
62
+ if (!rows.some((p) => sameResolvedPath(p.root, ctx.root))) rows.unshift(projectRecord(ctx));
63
+ for (const project of rows) {
64
+ if (!projectContexts.has(project.root)) {
65
+ projectContexts.set(project.root, contextForProject(project.root, project.db, { json: ctx.json }));
66
+ }
67
+ }
68
+ return rows;
69
+ }
70
+
71
+ function resolveWebProjectContext(root, db) {
72
+ const first = resolveProjectDatabase({ root, db, createStateDir: true });
73
+ // Re-read every filesystem identity immediately before the returned
74
+ // context can reach connect(). A path rebound after the first pass fails
75
+ // closed instead of carrying a stale lexical decision into SQLite.
76
+ const final = resolveProjectDatabase({
77
+ root: first.root,
78
+ db: first.db,
79
+ createStateDir: true
80
+ });
81
+ return contextForProject(final.root, final.db, { cwd: final.root, json: ctx.json });
82
+ }
83
+
84
+ function connectWebProject(projectCtx, options = {}) {
85
+ const final = resolveProjectDatabase({
86
+ root: projectCtx.root,
87
+ db: projectCtx.dbPath,
88
+ createStateDir: options.create !== false
89
+ });
90
+ return connect(contextForProject(final.root, final.db, {
91
+ cwd: final.root,
92
+ json: projectCtx.json
93
+ }), options);
94
+ }
95
+
96
+ function projectFromRequest(req, url) {
97
+ const requestedRoot = url.searchParams.get('root') ||
98
+ url.searchParams.get('project') ||
99
+ req.headers['x-hcc-root'] ||
100
+ ctx.root;
101
+ const requestedDb = url.searchParams.get('db') ||
102
+ req.headers['x-hcc-db'] ||
103
+ path.join(requestedRoot, '.hello-cc', 'mesh.db');
104
+ return rememberProject(
105
+ resolveWebProjectContext(requestedRoot, requestedDb),
106
+ { activity: true }
107
+ );
108
+ }
109
+
110
+ function getSession(projectCtx, id, db = null) {
111
+ const direct = sessions.get(sessionKey(projectCtx, id));
112
+ if (direct) return direct;
113
+ for (const session of sessionsForProject(projectCtx)) {
114
+ if (session.peerId === id) return session;
115
+ }
116
+ if (db) {
117
+ for (const session of sessionsForProject(projectCtx)) {
118
+ if (resolveSessionPeerId(db, session) === id) return session;
119
+ }
120
+ }
121
+ return null;
122
+ }
123
+
124
+ function readActionToken(input, req) {
125
+ const headerToken = req.headers["x-hcc-session-token"];
126
+ return String(input.action_token || input.actionToken || headerToken || "").trim();
127
+ }
128
+
129
+ function resolveWebActionSession(projectCtx, peer, input, req) {
130
+ const db = connectWebProject(projectCtx);
131
+ let session;
132
+ try {
133
+ session = getSession(projectCtx, peer, db);
134
+ } finally {
135
+ db.close();
136
+ }
137
+ if (!session || session.status !== "running") {
138
+ throw new CliError("PEER_IDENTITY_REQUIRED", "Web peer action requires a running managed session for " + peer, { peer });
139
+ }
140
+ const actorPeer = session.peerId || peer;
141
+ if (actorPeer !== peer && session.id !== peer) {
142
+ throw new CliError("PEER_IDENTITY_MISMATCH", "Web peer action target " + peer + " does not match managed session " + actorPeer, {
143
+ peer, actor_peer: actorPeer, session_id: session.id
144
+ });
145
+ }
146
+ const provided = readActionToken(input, req);
147
+ const authorized = Boolean(provided) && [...(session.actionTokens || [])]
148
+ .some((candidate) => tokenMatches(provided, candidate));
149
+ if (!authorized) {
150
+ throw new CliError("PEER_IDENTITY_REQUIRED", "Web peer action for " + peer + " requires the managed session action token", { peer });
151
+ }
152
+ return actorPeer;
153
+ }
154
+
155
+ function knownPeerIds(projectCtx) {
156
+ const db = connectWebProject(projectCtx);
157
+ try {
158
+ return db.prepare("SELECT id FROM peers").all().map((row) => row.id);
159
+ } finally {
160
+ db.close();
161
+ }
162
+ }
163
+
164
+ function nextProjectSessionId(projectCtx, kind) {
165
+ return nextSessionId([
166
+ ...sessionsForProject(projectCtx).map((session) => session.id),
167
+ ...knownPeerIds(projectCtx)
168
+ ], kind);
169
+ }
170
+
171
+ return {
172
+ projectContexts,
173
+ newSessionActionToken, rememberProject, knownProjects,
174
+ resolveWebProjectContext, connectWebProject, projectFromRequest,
175
+ getSession, readActionToken, resolveWebActionSession,
176
+ knownPeerIds, nextProjectSessionId
177
+ };
178
+ }
@@ -0,0 +1,203 @@
1
+ // PTY-backed web sessions, extracted from lib/web/runtime-main.mjs.
2
+ // startPtySession spawns node-pty children as tracked peers; the input
3
+ // normalization helpers (options, peer id, backend choice) feed both the
4
+ // PTY and tmux session factories.
5
+
6
+ import path from 'node:path';
7
+ import { CliError } from '../shared/errors.mjs';
8
+ import process from 'node:process';
9
+ import {
10
+ buildPeerCommand,
11
+ defaultSessionCommand,
12
+ hasResumeOpts
13
+ } from '../integrations/providers.mjs';
14
+ import { providerSessionPeerId } from '../core/peers/session.mjs';
15
+ import { childSessionEnv } from '../core/sessions/launch.mjs';
16
+ import { detectBranch } from '../project-context.mjs';
17
+
18
+ export function createPtySessions(deps) {
19
+ const {
20
+ pty, ctx, sessions,
21
+ broadcast, closeSessionClients, sessionKey,
22
+ connectWebProject, nextProjectSessionId,
23
+ startTmuxManagedSession,
24
+ now, addEvent, auditPayload, refreshPeerIoHeartbeat,
25
+ upsertPeer, upsertCanonicalPeerBinding,
26
+ requestActorPeer, requestSource
27
+ } = deps;
28
+
29
+ function startPtySession(input) {
30
+ const pctx = input.projectCtx || ctx;
31
+ const kind = input.kind || 'shell';
32
+ const id = input.id || nextProjectSessionId(pctx, kind);
33
+ const actorPeer = requestActorPeer(input, id);
34
+ const auditSource = requestSource(input, 'web');
35
+ const key = sessionKey(pctx, id);
36
+ if (sessions.has(key) && sessions.get(key).status === 'running') {
37
+ return sessions.get(key);
38
+ }
39
+ const role = input.role || 'peer';
40
+ const command = input.command || defaultSessionCommand(kind);
41
+ const cwd = path.resolve(input.cwd || pctx.root);
42
+ const callerEnv = input.env && typeof input.env === 'object' ? input.env : process.env;
43
+ const shell = callerEnv.SHELL || process.env.SHELL || 'bash';
44
+ const env = childSessionEnv({
45
+ HCC_PEER: id,
46
+ HCC_ROOT: pctx.root,
47
+ HCC_DB: pctx.dbPath,
48
+ TERM: 'xterm-256color'
49
+ }, callerEnv);
50
+ const size = input.size || { cols: 100, rows: 30 };
51
+ const child = pty.spawn(shell, ['-c', command], {
52
+ name: 'xterm-256color',
53
+ cols: size.cols,
54
+ rows: size.rows,
55
+ cwd,
56
+ env
57
+ });
58
+ const session = {
59
+ id,
60
+ peerId: id,
61
+ actorPeer,
62
+ auditSource,
63
+ actionTokens: new Set(),
64
+ root: pctx.root,
65
+ ctx: pctx,
66
+ kind,
67
+ role,
68
+ command,
69
+ cwd,
70
+ pid: child.pid,
71
+ pty: child,
72
+ status: 'running',
73
+ createdAt: now(),
74
+ exitedAt: null,
75
+ buffer: '',
76
+ clients: new Set()
77
+ };
78
+ sessions.set(key, session);
79
+ const db = connectWebProject(pctx);
80
+ try {
81
+ upsertPeer(db, {
82
+ id,
83
+ kind,
84
+ role,
85
+ worktree: cwd,
86
+ branch: detectBranch(cwd),
87
+ pid: child.pid,
88
+ status: 'running',
89
+ capabilities: 'web-pty'
90
+ });
91
+ const canonical = upsertCanonicalPeerBinding(db, {
92
+ peer: id,
93
+ provider: input.binding?.provider || kind,
94
+ provider_session_id: input.binding?.provider_session_id || null,
95
+ provider_session_name: input.binding?.provider_session_name || null,
96
+ resume_mode: input.binding?.resume_mode || 'new',
97
+ resume_arg: input.binding?.resume_arg || null,
98
+ command,
99
+ transport: 'web-pty',
100
+ runtime_session_id: id
101
+ }, Boolean(input.force));
102
+ session.peerId = canonical.peer;
103
+ session.binding = { ...canonical.binding };
104
+ addEvent(db, 'web.session.started', actorPeer, null, auditPayload({
105
+ actor: actorPeer,
106
+ target: session.peerId || id,
107
+ source: auditSource,
108
+ admin: actorPeer !== (session.peerId || id),
109
+ command,
110
+ cwd,
111
+ pid: child.pid
112
+ }));
113
+ } finally {
114
+ db.close();
115
+ }
116
+ child.onData((data) => {
117
+ session.buffer += data;
118
+ if (session.buffer.length > 250000) session.buffer = session.buffer.slice(-200000);
119
+ broadcast(session, { type: 'data', data });
120
+ refreshPeerIoHeartbeat(session);
121
+ });
122
+ child.onExit((event) => {
123
+ session.status = 'exited';
124
+ session.exitedAt = now();
125
+ const db = connectWebProject(pctx);
126
+ try {
127
+ db.prepare('UPDATE peers SET status = ?, last_seen_at = ? WHERE id = ?').run('exited', now(), id);
128
+ addEvent(db, 'web.session.exited', session.actorPeer || id, null, auditPayload({
129
+ actor: session.actorPeer || id,
130
+ target: session.peerId || id,
131
+ source: session.auditSource || 'web',
132
+ ...event
133
+ }));
134
+ } finally {
135
+ db.close();
136
+ }
137
+ broadcast(session, { type: 'exit', event });
138
+ closeSessionClients(session);
139
+ });
140
+ return session;
141
+ }
142
+
143
+ function webSessionBuildOptions(input) {
144
+ const mode = input.mode || 'new';
145
+ if (mode === 'new') return {};
146
+ if (mode === 'resume') {
147
+ const resume = String(input.resume || '').trim();
148
+ if (!resume) throw new CliError('BAD_REQUEST', 'resume session required');
149
+ return { resume };
150
+ }
151
+ if (mode === 'last') return { last: true };
152
+ if (mode === 'continue') return { continue: true };
153
+ throw new CliError('BAD_REQUEST', `Unsupported session mode: ${mode}`);
154
+ }
155
+
156
+ function webSessionPeerId(projectCtx, kind, opts, input) {
157
+ if (input.id) return input.id;
158
+ if (opts.resume) return providerSessionPeerId(kind, opts.resume);
159
+ return nextProjectSessionId(projectCtx, kind);
160
+ }
161
+
162
+ function normalizeWebSessionInput(input) {
163
+ const pctx = input.projectCtx || ctx;
164
+ const kind = input.kind || 'shell';
165
+ if (!['claude', 'codex', 'shell'].includes(kind)) {
166
+ throw new CliError('BAD_REQUEST', `Unsupported session kind: ${kind}`);
167
+ }
168
+ if (input.command || input.binding) return { ...input, kind, projectCtx: pctx };
169
+
170
+ const opts = webSessionBuildOptions(input);
171
+ if (kind === 'shell' && hasResumeOpts(opts)) {
172
+ throw new CliError('BAD_REQUEST', 'Resume modes are only supported for codex and claude sessions');
173
+ }
174
+ if (kind !== 'claude' && opts.continue) {
175
+ throw new CliError('BAD_REQUEST', 'continue is only supported for claude sessions');
176
+ }
177
+ if (kind !== 'codex' && opts.last) {
178
+ throw new CliError('BAD_REQUEST', 'last is only supported for codex sessions');
179
+ }
180
+
181
+ const id = webSessionPeerId(pctx, kind, opts, input);
182
+ const built = buildPeerCommand(id, kind, opts, []);
183
+ return {
184
+ ...input,
185
+ id,
186
+ kind,
187
+ command: built.command,
188
+ binding: built.binding,
189
+ projectCtx: pctx
190
+ };
191
+ }
192
+
193
+ function startSession(input) {
194
+ const normalized = normalizeWebSessionInput(input);
195
+ if (normalized.backend === 'pty') return startPtySession(normalized);
196
+ return startTmuxManagedSession(normalized);
197
+ }
198
+
199
+ return {
200
+ startPtySession, webSessionBuildOptions, webSessionPeerId,
201
+ normalizeWebSessionInput, startSession
202
+ };
203
+ }