aicodeman 1.5.1 → 1.6.1

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 (135) hide show
  1. package/README.md +48 -12
  2. package/README.zh-CN.md +68 -12
  3. package/dist/remote-hosts.d.ts +61 -1
  4. package/dist/remote-hosts.d.ts.map +1 -1
  5. package/dist/remote-hosts.js +142 -0
  6. package/dist/remote-hosts.js.map +1 -1
  7. package/dist/remote-reconnect.d.ts +129 -0
  8. package/dist/remote-reconnect.d.ts.map +1 -0
  9. package/dist/remote-reconnect.js +119 -0
  10. package/dist/remote-reconnect.js.map +1 -0
  11. package/dist/services/unified-session-service.d.ts +13 -1
  12. package/dist/services/unified-session-service.d.ts.map +1 -1
  13. package/dist/services/unified-session-service.js +88 -3
  14. package/dist/services/unified-session-service.js.map +1 -1
  15. package/dist/session-order.d.ts +40 -0
  16. package/dist/session-order.d.ts.map +1 -0
  17. package/dist/session-order.js +67 -0
  18. package/dist/session-order.js.map +1 -0
  19. package/dist/session.d.ts +39 -0
  20. package/dist/session.d.ts.map +1 -1
  21. package/dist/session.js +87 -19
  22. package/dist/session.js.map +1 -1
  23. package/dist/state-store.d.ts +12 -0
  24. package/dist/state-store.d.ts.map +1 -1
  25. package/dist/state-store.js +33 -0
  26. package/dist/state-store.js.map +1 -1
  27. package/dist/tmux-manager.d.ts +66 -0
  28. package/dist/tmux-manager.d.ts.map +1 -1
  29. package/dist/tmux-manager.js +226 -2
  30. package/dist/tmux-manager.js.map +1 -1
  31. package/dist/types/app-state.d.ts +2 -0
  32. package/dist/types/app-state.d.ts.map +1 -1
  33. package/dist/types/app-state.js.map +1 -1
  34. package/dist/types/session.d.ts +42 -0
  35. package/dist/types/session.d.ts.map +1 -1
  36. package/dist/types/session.js.map +1 -1
  37. package/dist/web/public/admin-ui.js +303 -6
  38. package/dist/web/public/admin-ui.js.br +0 -0
  39. package/dist/web/public/admin-ui.js.gz +0 -0
  40. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  41. package/dist/web/public/app.9367fd20.js +38 -0
  42. package/dist/web/public/app.9367fd20.js.br +0 -0
  43. package/dist/web/public/app.9367fd20.js.gz +0 -0
  44. package/dist/web/public/{constants.ad3a8031.js → constants.21a0a83e.js} +9 -0
  45. package/dist/web/public/constants.21a0a83e.js.br +0 -0
  46. package/dist/web/public/{constants.ad3a8031.js.gz → constants.21a0a83e.js.gz} +0 -0
  47. package/dist/web/public/cron-ui.js.gz +0 -0
  48. package/dist/web/public/image-input.ee16ad88.js.gz +0 -0
  49. package/dist/web/public/index.html +34 -11
  50. package/dist/web/public/index.html.br +0 -0
  51. package/dist/web/public/index.html.gz +0 -0
  52. package/dist/web/public/input-cjk.63794d0b.js.gz +0 -0
  53. package/dist/web/public/keyboard-accessory.3f91acef.js.gz +0 -0
  54. package/dist/web/public/mobile-handlers.b5a667ef.js.gz +0 -0
  55. package/dist/web/public/mobile.6a53092b.css +1 -0
  56. package/dist/web/public/mobile.6a53092b.css.br +0 -0
  57. package/dist/web/public/mobile.6a53092b.css.gz +0 -0
  58. package/dist/web/public/notification-manager.ca9dc40d.js.gz +0 -0
  59. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  60. package/dist/web/public/{panels-ui.3c9b0465.js → panels-ui.58582d4c.js} +2 -2
  61. package/dist/web/public/panels-ui.58582d4c.js.br +0 -0
  62. package/dist/web/public/panels-ui.58582d4c.js.gz +0 -0
  63. package/dist/web/public/ralph-panel.6de2d0f8.js.gz +0 -0
  64. package/dist/web/public/ralph-wizard.13a1831e.js.gz +0 -0
  65. package/dist/web/public/respawn-ui.2d249da9.js.gz +0 -0
  66. package/dist/web/public/sanitize-html.bc7078d6.js.gz +0 -0
  67. package/dist/web/public/{session-ui.e5d4f9b5.js → session-ui.0cf86dd4.js} +30 -22
  68. package/dist/web/public/session-ui.0cf86dd4.js.br +0 -0
  69. package/dist/web/public/session-ui.0cf86dd4.js.gz +0 -0
  70. package/dist/web/public/{settings-ui.29214cf1.js → settings-ui.bc5f4792.js} +2 -2
  71. package/dist/web/public/settings-ui.bc5f4792.js.br +0 -0
  72. package/dist/web/public/settings-ui.bc5f4792.js.gz +0 -0
  73. package/dist/web/public/styles.e9dc275f.css +1 -0
  74. package/dist/web/public/styles.e9dc275f.css.br +0 -0
  75. package/dist/web/public/styles.e9dc275f.css.gz +0 -0
  76. package/dist/web/public/subagent-windows.69b60c35.js.gz +0 -0
  77. package/dist/web/public/sw.js.gz +0 -0
  78. package/dist/web/public/terminal-ui.9ea677e9.js +3 -0
  79. package/dist/web/public/terminal-ui.9ea677e9.js.br +0 -0
  80. package/dist/web/public/terminal-ui.9ea677e9.js.gz +0 -0
  81. package/dist/web/public/ultracode-panel.js.gz +0 -0
  82. package/dist/web/public/ultracode-windows.js.gz +0 -0
  83. package/dist/web/public/upload.html.gz +0 -0
  84. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  85. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  86. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  87. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  88. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  89. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  90. package/dist/web/public/vendor/xterm-zerolag-input.137ad9f0.js.gz +0 -0
  91. package/dist/web/public/vendor/xterm.css.gz +0 -0
  92. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  93. package/dist/web/public/voice-input.085e9e73.js.gz +0 -0
  94. package/dist/web/routes/admin-routes.d.ts +2 -0
  95. package/dist/web/routes/admin-routes.d.ts.map +1 -1
  96. package/dist/web/routes/admin-routes.js +91 -2
  97. package/dist/web/routes/admin-routes.js.map +1 -1
  98. package/dist/web/routes/case-routes.d.ts.map +1 -1
  99. package/dist/web/routes/case-routes.js +19 -1
  100. package/dist/web/routes/case-routes.js.map +1 -1
  101. package/dist/web/routes/session-routes.d.ts.map +1 -1
  102. package/dist/web/routes/session-routes.js +140 -3
  103. package/dist/web/routes/session-routes.js.map +1 -1
  104. package/dist/web/schemas.d.ts +13 -0
  105. package/dist/web/schemas.d.ts.map +1 -1
  106. package/dist/web/schemas.js +31 -0
  107. package/dist/web/schemas.js.map +1 -1
  108. package/dist/web/server.d.ts +9 -0
  109. package/dist/web/server.d.ts.map +1 -1
  110. package/dist/web/server.js +57 -1
  111. package/dist/web/server.js.map +1 -1
  112. package/dist/web/sse-events.d.ts +15 -0
  113. package/dist/web/sse-events.d.ts.map +1 -1
  114. package/dist/web/sse-events.js +18 -0
  115. package/dist/web/sse-events.js.map +1 -1
  116. package/package.json +1 -1
  117. package/dist/web/public/app.1ccca158.js +0 -38
  118. package/dist/web/public/app.1ccca158.js.br +0 -0
  119. package/dist/web/public/app.1ccca158.js.gz +0 -0
  120. package/dist/web/public/constants.ad3a8031.js.br +0 -0
  121. package/dist/web/public/mobile.597136a6.css +0 -1
  122. package/dist/web/public/mobile.597136a6.css.br +0 -0
  123. package/dist/web/public/mobile.597136a6.css.gz +0 -0
  124. package/dist/web/public/panels-ui.3c9b0465.js.br +0 -0
  125. package/dist/web/public/panels-ui.3c9b0465.js.gz +0 -0
  126. package/dist/web/public/session-ui.e5d4f9b5.js.br +0 -0
  127. package/dist/web/public/session-ui.e5d4f9b5.js.gz +0 -0
  128. package/dist/web/public/settings-ui.29214cf1.js.br +0 -0
  129. package/dist/web/public/settings-ui.29214cf1.js.gz +0 -0
  130. package/dist/web/public/styles.9e6cdb9d.css +0 -1
  131. package/dist/web/public/styles.9e6cdb9d.css.br +0 -0
  132. package/dist/web/public/styles.9e6cdb9d.css.gz +0 -0
  133. package/dist/web/public/terminal-ui.efda6083.js +0 -3
  134. package/dist/web/public/terminal-ui.efda6083.js.br +0 -0
  135. package/dist/web/public/terminal-ui.efda6083.js.gz +0 -0
@@ -12,7 +12,8 @@ import { randomBytes } from 'node:crypto';
12
12
  import { ApiErrorCode, createErrorResponse, getErrorMessage, } from '../../types.js';
13
13
  import { Session, isAltScreenStripMode } from '../../session.js';
14
14
  import { SseEvent } from '../sse-events.js';
15
- import { CreateSessionSchema, SessionNameSchema, SessionColorSchema, RunPromptSchema, SessionInputWithLimitSchema, ResizeSchema, AutoClearSchema, AutoCompactSchema, AutoResumeSchema, ImageWatcherSchema, FlickerFilterSchema, QuickRunSchema, QuickStartSchema, InteractiveStartSchema, } from '../schemas.js';
15
+ import { CreateSessionSchema, SessionNameSchema, SessionColorSchema, RunPromptSchema, SessionInputWithLimitSchema, ResizeSchema, AutoClearSchema, AutoCompactSchema, AutoResumeSchema, PinSessionSchema, ImageWatcherSchema, FlickerFilterSchema, QuickRunSchema, QuickStartSchema, InteractiveStartSchema, SessionOrderUpdateSchema, } from '../schemas.js';
16
+ import { mergeSessionOrder } from '../../session-order.js';
16
17
  import { autoConfigureRalph, canAccessOwned, CASES_DIR, findSessionOrFail, getAuthUser, isAdmin, isWorkingDirAllowed, ownerFor, parseBody, persistAndBroadcastSession, resolveCasesDir, sessionCapacityMessage, SETTINGS_PATH, validatePathWithinBase, } from '../route-helpers.js';
17
18
  import { canUsernameRunPrivilegedCommands, resolveClaudeModeForUsername } from '../../user-store.js';
18
19
  import { isMultiUserMode } from '../../config/multiuser.js';
@@ -27,7 +28,7 @@ import { RunSummaryTracker } from '../../run-summary.js';
27
28
  import { MAX_INPUT_LENGTH, MAX_SESSION_NAME_LENGTH } from '../../config/terminal-limits.js';
28
29
  import { MAX_PASTE_IMAGE_BYTES } from '../../config/buffer-limits.js';
29
30
  import { dataPath, getDataDir } from '../../config/instance.js';
30
- import { checkRemoteTmuxAvailable, readRemoteCases, readRemoteHosts, toSessionRemote } from '../../remote-hosts.js';
31
+ import { checkRemoteTmuxAvailable, readRemoteCases, readRemoteHosts, toAttachedSessionRemote, toSessionRemote, } from '../../remote-hosts.js';
31
32
  import { checkDockerAvailable, checkDockerConfigDrift, checkDockerTmuxAvailable, ensureAgentBaseImage, DEFAULT_AGENT_IMAGE, persistDockerCaseClaudeSessionId, readDockerCases, readDockerHosts, toSessionDocker, } from '../../docker-hosts.js';
32
33
  import { LRUMap } from '../../utils/lru-map.js';
33
34
  // Path to linked-cases registry (same file used by case-routes resolveCasePath)
@@ -244,6 +245,16 @@ export function registerSessionRoutes(app, ctx) {
244
245
  return list;
245
246
  return list.filter((s) => canAccessOwned(user, s.owner));
246
247
  });
248
+ // ========== Session Tab Order (global sync, COD-131) ==========
249
+ app.put('/api/session-order', async (req) => {
250
+ const { order } = parseBody(SessionOrderUpdateSchema, req.body, 'Invalid session order');
251
+ // Server is authoritative but never drops ids it knows about that the
252
+ // pushing device hadn't loaded yet — those fall to the end (mergeSessionOrder).
253
+ const merged = mergeSessionOrder(order, ctx.store.getSessionOrder());
254
+ ctx.store.setSessionOrder(merged);
255
+ ctx.broadcast(SseEvent.SessionOrderChanged, { order: merged });
256
+ return { success: true, data: { order: merged } };
257
+ });
247
258
  // ========== Session Creation ==========
248
259
  app.post('/api/sessions', async (req) => {
249
260
  const owner = ownerFor(req);
@@ -252,7 +263,21 @@ export function registerSessionRoutes(app, ctx) {
252
263
  if (capMsg)
253
264
  return createErrorResponse(ApiErrorCode.OPERATION_FAILED, capMsg);
254
265
  const body = parseBody(CreateSessionSchema, req.body);
255
- const workingDir = body.workingDir || process.cwd();
266
+ let workingDir = body.workingDir || process.cwd();
267
+ let remote = undefined;
268
+ // COD-105 — attach to a discovered (non-owned) remote tmux session. The
269
+ // remote session is already running, so we skip the tmux-prereq probe and
270
+ // build a NON-owned SessionRemote (detach-not-kill on close). Remote CASE
271
+ // creation (owned durable sessions) is handled by the dedicated case-create
272
+ // endpoint below, which #145 consolidated remote-host resolution into.
273
+ if (body.attachRemoteSession) {
274
+ const { hostId, remoteSessionName } = body.attachRemoteSession;
275
+ const host = (await readRemoteHosts(CODEMAN_CONFIG_DIR)).find((item) => item.id === hostId);
276
+ if (!host)
277
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, 'Remote host not found');
278
+ workingDir = `${host.username}@${host.host}:${remoteSessionName}`;
279
+ remote = toAttachedSessionRemote(host, remoteSessionName, workingDir);
280
+ }
256
281
  // Multi-user: shell mode is arbitrary command execution as the host account,
257
282
  // gated behind the same grant as bypass (section 6.3). Resolve the owner's grant
258
283
  // from the store so a GRANTED regular user is not wrongly denied (AuthUser role alone can't tell).
@@ -407,6 +432,7 @@ export function registerSessionRoutes(app, ctx) {
407
432
  envOverrides: body.envOverrides,
408
433
  effort: body.effort,
409
434
  tmuxHistoryLimit: terminalHistoryConfig.tmuxHistoryLimit,
435
+ remote,
410
436
  owner,
411
437
  });
412
438
  ctx.addSession(session);
@@ -1403,6 +1429,45 @@ export function registerSessionRoutes(app, ctx) {
1403
1429
  },
1404
1430
  };
1405
1431
  });
1432
+ // ========== Pin (float to top of the session manager list, COD-139) ==========
1433
+ app.post('/api/sessions/:id/pin', async (req) => {
1434
+ const { id } = req.params;
1435
+ const body = parseBody(PinSessionSchema, req.body, 'Invalid request body');
1436
+ const session = ctx.sessions.get(id);
1437
+ if (session) {
1438
+ if (!canAccessOwned(getAuthUser(req), session.owner)) {
1439
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, `Session ${id} not found`);
1440
+ }
1441
+ session.setPinned(body.pinned);
1442
+ // Persist + broadcast session:updated (keeps tabs/state consistent), then a
1443
+ // dedicated session:pinned event so the session manager list re-sorts live.
1444
+ persistAndBroadcastSession(ctx, session);
1445
+ ctx.broadcast(SseEvent.SessionPinned, {
1446
+ id,
1447
+ pinned: session.pinned,
1448
+ pinnedAt: session.pinnedAt ?? undefined,
1449
+ });
1450
+ return {
1451
+ success: true,
1452
+ data: {
1453
+ pinned: session.pinned,
1454
+ pinnedAt: session.pinnedAt ?? undefined,
1455
+ },
1456
+ };
1457
+ }
1458
+ // COD-142 keeps a pinned session's record after kill (demoteOrRemoveSession),
1459
+ // so pin toggles must also work WITHOUT a live Session — otherwise a
1460
+ // pinned-then-killed record could never be unpinned (cleanup skips pinned
1461
+ // records, and the record has no live session to route through).
1462
+ const persisted = ctx.store.getSession(id);
1463
+ if (!persisted || !canAccessOwned(getAuthUser(req), persisted.owner)) {
1464
+ return createErrorResponse(ApiErrorCode.NOT_FOUND, `Session ${id} not found`);
1465
+ }
1466
+ const pinnedAt = body.pinned ? Date.now() : undefined;
1467
+ ctx.store.setSession(id, { ...persisted, pinned: body.pinned || undefined, pinnedAt });
1468
+ ctx.broadcast(SseEvent.SessionPinned, { id, pinned: body.pinned, pinnedAt });
1469
+ return { success: true, data: { pinned: body.pinned, pinnedAt } };
1470
+ });
1406
1471
  // ========== Image Watcher ==========
1407
1472
  app.post('/api/sessions/:id/image-watcher', async (req) => {
1408
1473
  const { id } = req.params;
@@ -1919,6 +1984,63 @@ export function registerSessionRoutes(app, ctx) {
1919
1984
  }
1920
1985
  return undefined;
1921
1986
  }
1987
+ /**
1988
+ * Extract the text of the LAST user message from a JSONL transcript chunk
1989
+ * (COD-145). Mirrors `extractFirstUserPrompt` exactly — same user-message
1990
+ * detection, same noise/secret/slash-command filters, same 120-char cap — but
1991
+ * keeps the last qualifying match instead of returning on the first. Scan the
1992
+ * file tail for this (the most recent prompt lives near the end).
1993
+ */
1994
+ function extractLastUserPrompt(text) {
1995
+ const MAX_PROMPT_LEN = 120;
1996
+ let result;
1997
+ let start = 0;
1998
+ while (start < text.length) {
1999
+ const end = text.indexOf('\n', start);
2000
+ const line = end === -1 ? text.slice(start) : text.slice(start, end);
2001
+ start = end === -1 ? text.length : end + 1;
2002
+ if (!line.includes('"type":"user"'))
2003
+ continue;
2004
+ try {
2005
+ const entry = JSON.parse(line);
2006
+ if (entry.type !== 'user' || !entry.message)
2007
+ continue;
2008
+ const content = entry.message.content;
2009
+ let msgText;
2010
+ if (typeof content === 'string') {
2011
+ msgText = content;
2012
+ }
2013
+ else if (Array.isArray(content)) {
2014
+ const textBlock = content.find((b) => b.type === 'text');
2015
+ if (textBlock)
2016
+ msgText = textBlock.text;
2017
+ }
2018
+ if (!msgText)
2019
+ continue;
2020
+ msgText = msgText
2021
+ .replace(/<[^>]+>/g, '')
2022
+ .replace(new RegExp(String.raw `\x1b\[[0-9;]*[a-zA-Z]`, 'g'), '')
2023
+ .trim()
2024
+ .replace(/\s+/g, ' ');
2025
+ if (!msgText)
2026
+ continue;
2027
+ if (/^(Caveat:|init\b|clear\b|resume\b|\/[a-z][\w-]*\b|You are a |\[Request |Set model to )/i.test(msgText) ||
2028
+ /^(Please )?(analyze|review) this codebase/i.test(msgText) ||
2029
+ /^(Read|Implement the following) .+, then (search|list|check) /i.test(msgText) ||
2030
+ /^\d+ vulnerabilit/i.test(msgText) ||
2031
+ /\btoolu_/.test(msgText) ||
2032
+ /^[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]+/.test(msgText) ||
2033
+ /\b(sk-ant-|ANTHROPIC_API_KEY|API_KEY=|SECRET|TOKEN=)/i.test(msgText) ||
2034
+ msgText.length < 8)
2035
+ continue;
2036
+ result = msgText.length > MAX_PROMPT_LEN ? msgText.slice(0, MAX_PROMPT_LEN) + '…' : msgText;
2037
+ }
2038
+ catch {
2039
+ // Malformed line — skip
2040
+ }
2041
+ }
2042
+ return result;
2043
+ }
1922
2044
  /**
1923
2045
  * Decode a Claude project key (e.g. "-Users-teigen-Documents-Workspace-AI-project-Mirror")
1924
2046
  * back to a filesystem path ("/Users/teigen/Documents/Workspace/AI_project/Mirror").
@@ -2096,6 +2218,15 @@ export function registerSessionRoutes(app, ctx) {
2096
2218
  if (tail)
2097
2219
  firstPrompt = extractFirstUserPrompt(tail);
2098
2220
  }
2221
+ // COD-145: last (most recent) user prompt lives near the END of the file, so
2222
+ // prefer the tail. For large files where no tail was read yet, read one
2223
+ // (mirrors the firstPrompt > 65536 block). Small files fit in `head`, which
2224
+ // then contains the whole transcript — scan it for the last match instead.
2225
+ if (!tail && fileStat.size > 65536) {
2226
+ const tailBuf = Buffer.alloc(32768);
2227
+ tail = await readFileTail(filePath, tailBuf, fileStat.size);
2228
+ }
2229
+ const lastPrompt = (tail ? extractLastUserPrompt(tail) : undefined) ?? (head ? extractLastUserPrompt(head) : undefined);
2099
2230
  out.push({
2100
2231
  sessionId,
2101
2232
  workingDir,
@@ -2103,6 +2234,7 @@ export function registerSessionRoutes(app, ctx) {
2103
2234
  sizeBytes: fileStat.size,
2104
2235
  lastModified: fileStat.mtime.toISOString(),
2105
2236
  firstPrompt,
2237
+ lastPrompt,
2106
2238
  });
2107
2239
  }
2108
2240
  return out;
@@ -2174,6 +2306,8 @@ export function registerSessionRoutes(app, ctx) {
2174
2306
  createdAt: st.createdAt,
2175
2307
  lastActivityAt: st.lastActivityAt,
2176
2308
  claudeSessionId: s.claudeSessionId ?? undefined,
2309
+ pinned: st.pinned,
2310
+ pinnedAt: st.pinnedAt,
2177
2311
  };
2178
2312
  });
2179
2313
  // Persisted sessions (state.json). resumeSessionId is the Claude
@@ -2188,6 +2322,8 @@ export function registerSessionRoutes(app, ctx) {
2188
2322
  createdAt: p.createdAt,
2189
2323
  lastActivityAt: p.lastActivityAt,
2190
2324
  claudeSessionId: p.resumeSessionId,
2325
+ pinned: p.pinned,
2326
+ pinnedAt: p.pinnedAt,
2191
2327
  }));
2192
2328
  // Lifecycle audit log (newest-first, capped).
2193
2329
  let lifecycle = [];
@@ -2220,6 +2356,7 @@ export function registerSessionRoutes(app, ctx) {
2220
2356
  sizeBytes: h.sizeBytes,
2221
2357
  lastModified: h.lastModified,
2222
2358
  firstPrompt: h.firstPrompt,
2359
+ lastPrompt: h.lastPrompt,
2223
2360
  projectKey: h.projectKey,
2224
2361
  });
2225
2362
  }