claude-code-session-manager 0.33.0 → 0.34.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 (41) hide show
  1. package/README.md +5 -1
  2. package/dist/assets/{TiptapBody-DHzFmRr2.js → TiptapBody-DWeWI8gw.js} +51 -51
  3. package/dist/assets/index-C2m4dco8.css +32 -0
  4. package/dist/assets/index-CFT773vM.js +3491 -0
  5. package/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/plugins/session-manager-dev/skills/discover-features/SKILL.md +95 -0
  8. package/plugins/session-manager-dev/skills/optimize-kpi/SKILL.md +9 -9
  9. package/plugins/session-manager-dev/skills/process-feedback/SKILL.md +77 -62
  10. package/plugins/session-manager-dev/skills/project-status/SKILL.md +13 -0
  11. package/src/main/__tests__/chat-queue.test.cjs +65 -0
  12. package/src/main/__tests__/chat-stop-signal.test.cjs +52 -0
  13. package/src/main/__tests__/exchanges.test.cjs +177 -0
  14. package/src/main/__tests__/files-reject-credentials.test.cjs +40 -0
  15. package/src/main/__tests__/kg-augment.test.cjs +195 -0
  16. package/src/main/__tests__/memoryAggregate.test.cjs +70 -0
  17. package/src/main/agentMemory.cjs +0 -21
  18. package/src/main/chatRunner.cjs +393 -0
  19. package/src/main/exchanges.cjs +125 -0
  20. package/src/main/files.cjs +20 -8
  21. package/src/main/historyAggregator.cjs +0 -162
  22. package/src/main/index.cjs +76 -57
  23. package/src/main/ipcSchemas.cjs +70 -30
  24. package/src/main/lib/kgExchangePairing.cjs +75 -0
  25. package/src/main/lib/reaperHelpers.cjs +7 -1
  26. package/src/main/lib/summarize.cjs +114 -0
  27. package/src/main/memoryAggregate.cjs +250 -0
  28. package/src/main/pluginInstall.cjs +4 -2
  29. package/src/main/scheduler.cjs +66 -20
  30. package/src/main/supervisor.cjs +16 -0
  31. package/src/main/transcripts.cjs +22 -2
  32. package/src/main/usage.cjs +58 -11
  33. package/src/main/voiceHotkey.cjs +0 -2
  34. package/src/main/webRemote.cjs +11 -78
  35. package/src/preload/api.d.ts +131 -125
  36. package/src/preload/index.cjs +45 -29
  37. package/dist/assets/index-AKeGl-VM.css +0 -32
  38. package/dist/assets/index-Bwwbc2mS.js +0 -3535
  39. package/src/main/kg.cjs +0 -792
  40. package/src/main/lib/kgLite.cjs +0 -195
  41. package/src/main/lib/kgPrune.cjs +0 -87
@@ -16,6 +16,7 @@
16
16
  * }
17
17
  */
18
18
 
19
+ const fs = require('node:fs');
19
20
  const fsp = require('node:fs/promises');
20
21
  const path = require('node:path');
21
22
  const os = require('node:os');
@@ -30,28 +31,74 @@ function envEnabled(v) {
30
31
  return v != null && v !== '' && v !== '0' && String(v).toLowerCase() !== 'false';
31
32
  }
32
33
 
34
+ /**
35
+ * Reads enterprise-auth signals from Claude Code's own settings files — the
36
+ * `env` block and `apiKeyHelper`. This is where corporate gateways are usually
37
+ * configured (a managed/enterprise policy or the user's settings.json), NOT as
38
+ * exported shell vars. It matters most on macOS, where a GUI-launched app does
39
+ * not inherit the shell's environment, so `process.env` looks like a clean
40
+ * consumer install even when `claude` itself is talking to a gateway.
41
+ *
42
+ * Precedence mirrors Claude Code: managed (enterprise) settings, then user
43
+ * settings, then user-local settings. Missing/unreadable/invalid files are
44
+ * skipped. Returns `{ env, apiKeyHelper }`.
45
+ */
46
+ function readClaudeSettingsAuth() {
47
+ const merged = {};
48
+ let apiKeyHelper = false;
49
+ const managed = process.platform === 'darwin'
50
+ ? '/Library/Application Support/ClaudeCode/managed-settings.json'
51
+ : '/etc/claude-code/managed-settings.json';
52
+ const files = [
53
+ managed,
54
+ path.join(os.homedir(), '.claude', 'settings.json'),
55
+ path.join(os.homedir(), '.claude', 'settings.local.json'),
56
+ ];
57
+ for (const f of files) {
58
+ try {
59
+ const data = JSON.parse(fs.readFileSync(f, 'utf8'));
60
+ if (data && typeof data.env === 'object' && data.env) Object.assign(merged, data.env);
61
+ if (data && data.apiKeyHelper) apiKeyHelper = true;
62
+ } catch { /* missing / unreadable / invalid JSON → skip */ }
63
+ }
64
+ return { env: merged, apiKeyHelper };
65
+ }
66
+
33
67
  /**
34
68
  * Is the consumer 5-hour usage meter (/api/oauth/usage) even applicable here?
35
69
  *
36
70
  * That endpoint only exists for OAuth/subscription auth against
37
71
  * api.anthropic.com. Enterprise auth modes have no such meter, so polling it
38
- * just 404s/times-out and the scheduler must NOT gate on (or pause for) it.
39
- * Detected modes: Amazon Bedrock, Google Vertex, raw API-key, a custom auth
40
- * token, or a non-Anthropic base URL (corporate gateway/proxy).
72
+ * just 404s/times-out (or 401s with no credentials file) and the scheduler
73
+ * must NOT gate on (or pause for) it. Detected modes: Amazon Bedrock, Google
74
+ * Vertex, raw API-key, a custom auth token, a non-Anthropic base URL (corporate
75
+ * gateway/proxy), or an `apiKeyHelper` script.
76
+ *
77
+ * Signals are read from BOTH process.env and Claude Code's settings files, so a
78
+ * gateway configured purely in settings.json (the common enterprise case) is
79
+ * detected even when the GUI process inherited a clean environment.
41
80
  *
42
81
  * Returns false → caller should treat usage as unavailable-by-design and fire
43
82
  * work on its own (pending + memory) instead of waiting on a meter.
44
83
  */
45
- function usageMeterApplicable(env = process.env) {
46
- if (envEnabled(env.CLAUDE_CODE_USE_BEDROCK)) return false;
47
- if (envEnabled(env.CLAUDE_CODE_USE_VERTEX)) return false;
48
- if (env.ANTHROPIC_API_KEY) return false;
49
- if (env.ANTHROPIC_AUTH_TOKEN) return false;
50
- if (env.ANTHROPIC_BASE_URL) {
84
+ function usageMeterApplicable(env = process.env, settings = readClaudeSettingsAuth()) {
85
+ // Manual escape hatch: if detection misses an unusual gateway setup, the user
86
+ // can force "no consumer meter" so the scheduler stops pausing on 'auth'.
87
+ if (envEnabled(env.SM_NO_USAGE_METER)) return false;
88
+ // An apiKeyHelper means `claude` mints its own key → non-OAuth, no meter.
89
+ if (settings && settings.apiKeyHelper) return false;
90
+ // Effective env: settings.json provides values the GUI process didn't
91
+ // inherit; a real process.env var wins when both define the same key.
92
+ const eff = { ...(settings && settings.env), ...env };
93
+ if (envEnabled(eff.CLAUDE_CODE_USE_BEDROCK)) return false;
94
+ if (envEnabled(eff.CLAUDE_CODE_USE_VERTEX)) return false;
95
+ if (eff.ANTHROPIC_API_KEY) return false;
96
+ if (eff.ANTHROPIC_AUTH_TOKEN) return false;
97
+ if (eff.ANTHROPIC_BASE_URL) {
51
98
  // Parse the host rather than substring-match, so a deceptive gateway like
52
99
  // https://anthropic.com.attacker.example is correctly treated as enterprise.
53
100
  let host;
54
- try { host = new URL(env.ANTHROPIC_BASE_URL).hostname.toLowerCase(); }
101
+ try { host = new URL(eff.ANTHROPIC_BASE_URL).hostname.toLowerCase(); }
55
102
  catch { return false; } // unparseable custom URL → treat as a gateway
56
103
  if (host !== 'anthropic.com' && !host.endsWith('.anthropic.com')) return false;
57
104
  }
@@ -197,4 +244,4 @@ function registerBillingHandlers() {
197
244
  });
198
245
  }
199
246
 
200
- module.exports = { registerBillingHandlers, fetchUsage, classifyUsageResponse, usageMeterApplicable };
247
+ module.exports = { registerBillingHandlers, fetchUsage, classifyUsageResponse, usageMeterApplicable, readClaudeSettingsAuth };
@@ -313,8 +313,6 @@ function registerHotkeyHandlers() {
313
313
  return { ok: true, config: currentConfig };
314
314
  }));
315
315
 
316
- ipcMain.handle('voice:get-hotkey-config-path', () => voiceSettings.storePath());
317
-
318
316
  // F5 — device picker preference (additive subtree on voice.json).
319
317
  ipcMain.handle('voice:get-device-pref', async () => {
320
318
  return await voiceSettings.loadDevice();
@@ -28,6 +28,7 @@ const { writeTextAtomic, validatePath } = require('./config.cjs');
28
28
  const logs = require('./logs.cjs');
29
29
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
30
30
  const { schemas } = require('./ipcSchemas.cjs');
31
+ const { summarize: _sharedSummarize } = require('./lib/summarize.cjs');
31
32
  const { makeState, confirmSas: confirmSasLogic } = require('./lib/e2eStateMachine.cjs');
32
33
 
33
34
  // ─── Constants ───────────────────────────────────────────────────────────────
@@ -740,70 +741,9 @@ function startSessionListPush() {
740
741
  }
741
742
 
742
743
  // ─── SM-V2-03: mobile summary via Claude Haiku 4.5 ───────────────────────────
744
+ // Summarization logic lives in lib/summarize.cjs (shared with exchanges.cjs).
743
745
 
744
- const SUMMARY_MIN_CHARS = 280; // below this, push raw — not worth an API call
745
- const SUMMARY_MODEL = 'claude-haiku-4-5';
746
- const SUMMARY_MAX_INPUT_CHARS = 24_000; // cap the turn text sent to Haiku (~6k tokens)
747
- const SUMMARY_SYSTEM =
748
- 'Summarize this Claude Code assistant turn for a phone screen in 2 sentences max, ' +
749
- 'followed by an optional list of up to 3 short action items. Plain text only — no ' +
750
- 'markdown headers, no code blocks. Lead with what was done or decided.';
751
-
752
- let _anthropicKeyCache = null; // memoized found key only (string); null = re-resolve
753
-
754
- /** Resolve the Anthropic API key: env → web-remote.json → null (degrade to raw).
755
- * Only a FOUND key is cached — if absent we re-resolve each call (cheap, loadConfig
756
- * is TTL-cached) so adding the key to web-remote.json later takes effect without a restart. */
757
- async function resolveAnthropicKey() {
758
- if (_anthropicKeyCache) return _anthropicKeyCache;
759
- const fromEnv = process.env.ANTHROPIC_API_KEY;
760
- if (fromEnv && fromEnv.trim()) { _anthropicKeyCache = fromEnv.trim(); return _anthropicKeyCache; }
761
- try {
762
- const cfg = await loadConfig();
763
- const k = cfg.anthropicApiKey;
764
- if (typeof k === 'string' && k.trim()) { _anthropicKeyCache = k.trim(); return _anthropicKeyCache; }
765
- } catch { /* fall through to null → re-resolve next time */ }
766
- return null;
767
- }
768
-
769
- /** POST to the Anthropic Messages API. Returns the first text block, or throws. */
770
- function anthropicSummarize(apiKey, text) {
771
- const body = JSON.stringify({
772
- model: SUMMARY_MODEL,
773
- max_tokens: 320,
774
- system: SUMMARY_SYSTEM,
775
- messages: [{ role: 'user', content: text.slice(0, SUMMARY_MAX_INPUT_CHARS) }],
776
- });
777
- return new Promise((resolve, reject) => {
778
- const req = https.request('https://api.anthropic.com/v1/messages', {
779
- method: 'POST',
780
- headers: {
781
- 'content-type': 'application/json',
782
- 'x-api-key': apiKey,
783
- 'anthropic-version': '2023-06-01',
784
- 'content-length': Buffer.byteLength(body),
785
- },
786
- timeout: 20_000,
787
- }, (res) => {
788
- let data = '';
789
- res.on('data', (c) => { data += c; });
790
- res.on('end', () => {
791
- if (res.statusCode < 200 || res.statusCode >= 300) {
792
- return reject(new Error(`anthropic HTTP ${res.statusCode}`));
793
- }
794
- try {
795
- const json = JSON.parse(data);
796
- const block = Array.isArray(json.content) ? json.content.find((b) => b.type === 'text') : null;
797
- if (!block?.text) return reject(new Error('no text in response'));
798
- resolve(block.text.trim());
799
- } catch (e) { reject(e); }
800
- });
801
- });
802
- req.on('error', reject);
803
- req.on('timeout', () => req.destroy(new Error('anthropic request timed out')));
804
- req.end(body);
805
- });
806
- }
746
+ const SUMMARY_MIN_CHARS = 280; // below this, push raw — not worth an API call
807
747
 
808
748
  /**
809
749
  * Produce a mobile summary of the watcher's last completed assistant turn and push it.
@@ -823,22 +763,15 @@ async function maybeSummarize(w) {
823
763
  return;
824
764
  }
825
765
 
826
- const apiKey = await resolveAnthropicKey();
827
- if (!apiKey) {
828
- // Degrade gracefully: push a trimmed raw message + a hint flag the app can surface.
829
- pushEvent('event:session:summary', {
830
- tabId: w.tabId, summary: text.slice(0, 600), ofMessageId, model: 'raw', degraded: 'no_api_key', ts: Date.now(),
831
- });
832
- return;
833
- }
834
-
835
- try {
836
- const summary = await anthropicSummarize(apiKey, text);
837
- pushEvent('event:session:summary', { tabId: w.tabId, summary, ofMessageId, model: SUMMARY_MODEL, ts: Date.now() });
838
- } catch (e) {
839
- logs.writeLine({ scope: 'webRemote', level: 'warn', message: 'summary failed; pushing raw', meta: { error: e?.message } });
840
- pushEvent('event:session:summary', { tabId: w.tabId, summary: text.slice(0, 600), ofMessageId, model: 'raw', degraded: 'api_error', ts: Date.now() });
766
+ const { summary, model, degraded } = await _sharedSummarize(text);
767
+ if (degraded) {
768
+ logs.writeLine({ scope: 'webRemote', level: 'warn', message: `summary degraded: ${degraded}`, meta: {} });
841
769
  }
770
+ pushEvent('event:session:summary', {
771
+ tabId: w.tabId, summary, ofMessageId, model,
772
+ ...(degraded ? { degraded } : {}),
773
+ ts: Date.now(),
774
+ });
842
775
  }
843
776
 
844
777
  // ─── Message handling & command dispatch ─────────────────────────────────────
@@ -443,24 +443,6 @@ export interface HistoryAggregateResult {
443
443
  scannedMs: number;
444
444
  }
445
445
 
446
- export interface ConversationSummary {
447
- /** ISO 8601 timestamp of the first event in the conversation (or file mtime fallback). */
448
- timestamp: string;
449
- /** Decoded project cwd, e.g. /home/user/Projects/foo. */
450
- projectFolder: string;
451
- stats: {
452
- /** Wall-clock duration in ms (first event ts → last event ts). Omitted when unknown. */
453
- duration?: number;
454
- /** Sum of input + output tokens across the file. 0 when no usage blocks present. */
455
- estimatedTokens: number;
456
- };
457
- }
458
-
459
- export interface ListConversationsResult {
460
- conversations: ConversationSummary[];
461
- scannedMs: number;
462
- }
463
-
464
446
  export interface SessionScanEntry {
465
447
  sessionId: string;
466
448
  projectEncoded: string;
@@ -490,7 +472,6 @@ export interface FilesWriteResult { ok: boolean; error: string | null }
490
472
  export interface FilesCreateResult { ok: boolean; path?: string; error: string | null }
491
473
  export interface FilesRenameResult { ok: boolean; newPath?: string; error: string | null }
492
474
  export interface FilesDeleteResult { ok: boolean; error: string | null }
493
- export interface FilesShellResult { ok: boolean; error?: string }
494
475
 
495
476
  export interface SearchFileEntry {
496
477
  name: string;
@@ -681,6 +662,29 @@ export interface MemoryMutationResult {
681
662
  error: string | null;
682
663
  }
683
664
 
665
+ export interface MemoryClusterLink {
666
+ from: string;
667
+ to: string;
668
+ label?: string;
669
+ }
670
+
671
+ export interface MemoryCluster {
672
+ id: string;
673
+ name: string;
674
+ summary: string;
675
+ memberSlugs: string[];
676
+ links: MemoryClusterLink[];
677
+ }
678
+
679
+ export interface MemoryAggregateResult {
680
+ workspace: string;
681
+ generatedAt: number | null;
682
+ clusters: MemoryCluster[];
683
+ orphans: string[];
684
+ cached: boolean;
685
+ error?: string;
686
+ }
687
+
684
688
  // ────────────────────────────────────────────── Per-subagent memory
685
689
  // Stored at ~/.claude/session-manager/agent-memory/<agentId>.json. Keyed by
686
690
  // agent name (the .md filename in ~/.claude/agents/), not by workspace cwd.
@@ -712,17 +716,6 @@ export interface AgentMemoryMutationResult {
712
716
  error: string | null;
713
717
  }
714
718
 
715
- export interface AgentMemoryAgentSummary {
716
- agentId: string;
717
- bytes: number;
718
- mtimeMs: number;
719
- }
720
-
721
- export interface AgentMemoryListAgentsResult {
722
- agents: AgentMemoryAgentSummary[];
723
- error: string | null;
724
- }
725
-
726
719
  // ────────────────────────────────────────────── Git status (richer than app:git-branch)
727
720
 
728
721
  /** Mirrors the status returned by src/main/git.cjs mapStatus(). */
@@ -845,6 +838,59 @@ export interface WebRemoteAuditTailResult {
845
838
  error?: string;
846
839
  }
847
840
 
841
+ // ────────────────────────────────────────────── Chat runner (PRD 319)
842
+
843
+ export interface ChatRunPayload {
844
+ tabId: string;
845
+ /** UUID for the claude session — created by the caller; stays the same across
846
+ * resumes so transcript context carries forward. */
847
+ sessionId: string;
848
+ prompt: string;
849
+ cwd: string;
850
+ /** When true, use --resume <sessionId> instead of --session-id. */
851
+ resume?: boolean;
852
+ }
853
+
854
+ export interface ChatRunQueuedEvent {
855
+ tabId: string;
856
+ sessionId: string;
857
+ /** 1-based position in the FIFO queue while waiting behind a busy lane. */
858
+ position: number;
859
+ }
860
+
861
+ export interface ChatRunStartedEvent {
862
+ tabId: string;
863
+ sessionId: string;
864
+ }
865
+
866
+ export interface ChatRunOutputEvent {
867
+ tabId: string;
868
+ /** Incremental text delta from one assistant content block. */
869
+ delta: string;
870
+ }
871
+
872
+ export interface ChatRunNeedsInputEvent {
873
+ tabId: string;
874
+ sessionId: string;
875
+ /** Questions the agent is blocked on. */
876
+ questions: string[];
877
+ /** Full raw assistant text including the stop-signal sentinel. */
878
+ raw: string;
879
+ }
880
+
881
+ export interface ChatRunCompleteEvent {
882
+ tabId: string;
883
+ sessionId: string;
884
+ /** The run's own final assistant message verbatim. */
885
+ finalMessage: string;
886
+ }
887
+
888
+ export interface ChatRunErrorEvent {
889
+ tabId: string;
890
+ sessionId: string;
891
+ message: string;
892
+ }
893
+
848
894
  export interface SessionManagerAPI {
849
895
  app: {
850
896
  version: () => Promise<string>;
@@ -867,16 +913,6 @@ export interface SessionManagerAPI {
867
913
  homeSelfCheck: () => Promise<{ ok: boolean; error?: string; realCwd?: string }>;
868
914
  onNewSession: (handler: () => void) => () => void;
869
915
  onRebootSession: (handler: () => void) => () => void;
870
- openInEditor: (cwd: string, editor?: string | null) => Promise<{ ok: boolean; opener?: string; error?: string }>;
871
- /** Open an http/https URL in the OS default browser. file://, javascript:,
872
- * and other schemes are rejected with `ok:false` to prevent abuse. */
873
- openExternal: (url: string) => Promise<{ ok: boolean; error?: string }>;
874
- /** Open a specific file at line:col in the user's editor. Editors with
875
- * goto-line support (code/cursor/subl) get the `-g file:line:col` form;
876
- * others open the file alone. Image files are routed to the OS default viewer. */
877
- openFileInEditor: (filePath: string, line?: number, col?: number, editor?: string | null) => Promise<{ ok: boolean; opener?: string; error?: string }>;
878
- openInFinder: (cwd: string) => Promise<{ ok: boolean; opener?: string; error?: string }>;
879
- openInTerminal: (cwd: string) => Promise<{ ok: boolean; opener?: string; error?: string }>;
880
916
  archiveProject: (encoded: string) => Promise<{ ok: boolean; error?: string }>;
881
917
  };
882
918
  pty: {
@@ -929,7 +965,6 @@ export interface SessionManagerAPI {
929
965
  onHotkeyConfigChanged: (handler: (cfg: VoiceHotkeyConfig) => void) => () => void;
930
966
  getHotkeyConfig: () => Promise<VoiceHotkeyConfig>;
931
967
  setHotkeyConfig: (cfg: VoiceHotkeyConfig) => Promise<VoiceSetHotkeyResult>;
932
- getHotkeyConfigPath: () => Promise<string>;
933
968
  setRecording: (recording: boolean) => void;
934
969
  /** F5: read persisted audio-input device preference. */
935
970
  getDevicePref: () => Promise<VoiceDevicePref>;
@@ -965,8 +1000,21 @@ export interface SessionManagerAPI {
965
1000
  create: (parentPath: string, name: string, kind: 'file' | 'folder') => Promise<FilesCreateResult>;
966
1001
  rename: (path: string, newName: string) => Promise<FilesRenameResult>;
967
1002
  delete: (path: string) => Promise<FilesDeleteResult>;
968
- openExternal: (path: string) => Promise<FilesShellResult>;
969
- showInFinder: (path: string) => Promise<FilesShellResult>;
1003
+ };
1004
+ /** Consolidated shell open/reveal. One method, discriminated on `as`, replaces
1005
+ * the former app.openIn* / app.openExternal / files.openExternal / files.showInFinder.
1006
+ * Each variant's boundary guard (home-scope / http(s)-only) runs in the main handler. */
1007
+ shell: {
1008
+ open: (
1009
+ opts:
1010
+ | { as: 'editor'; cwd: string; editor?: string | null }
1011
+ | { as: 'fileInEditor'; path: string; line?: number; col?: number; editor?: string | null }
1012
+ | { as: 'finder'; cwd: string }
1013
+ | { as: 'terminal'; cwd: string }
1014
+ | { as: 'external'; url: string }
1015
+ | { as: 'openPath'; path: string }
1016
+ | { as: 'revealPath'; path: string }
1017
+ ) => Promise<{ ok: boolean; opener?: string; error?: string }>;
970
1018
  };
971
1019
  search: {
972
1020
  files: (cwd: string, query?: string, opts?: { limit?: number }) => Promise<SearchFilesResult>;
@@ -983,7 +1031,6 @@ export interface SessionManagerAPI {
983
1031
  };
984
1032
  history: {
985
1033
  aggregate: (req?: HistoryAggregateRequest) => Promise<HistoryAggregateResult>;
986
- listConversations: () => Promise<ListConversationsResult>;
987
1034
  scanProjects: () => Promise<SessionScanResult>;
988
1035
  };
989
1036
  schedule: {
@@ -993,7 +1040,6 @@ export interface SessionManagerAPI {
993
1040
  runNow: () => Promise<{ ok: boolean }>;
994
1041
  forceTick: () => Promise<{ ok: boolean }>;
995
1042
  resume: () => Promise<{ ok: boolean }>;
996
- refreshReset: () => Promise<{ ok: boolean; nextReset: string | null }>;
997
1043
  /** Re-scan prds/ and merge into queue.json; broadcasts updated state. */
998
1044
  rescan: () => Promise<{ ok: boolean }>;
999
1045
  /** Move all pending+failed PRDs to prds-archived/<ISO>/ and drop their
@@ -1057,6 +1103,8 @@ export interface SessionManagerAPI {
1057
1103
  delete: (name: string, workspace?: string) => Promise<MemoryMutationResult>;
1058
1104
  /** Create a new memory entry with starter frontmatter + body. */
1059
1105
  create: (name: string, description?: string, workspace?: string) => Promise<MemoryMutationResult>;
1106
+ /** Aggregate workspace memories into semantic clusters. `refresh:true` fires a cost-gated `claude -p` pass; otherwise returns the cache only. */
1107
+ aggregate: (workspace: string, refresh?: boolean) => Promise<MemoryAggregateResult>;
1060
1108
  };
1061
1109
  agentMemory: {
1062
1110
  /** List all memory entries for one subagent. Sorted newest first. */
@@ -1072,8 +1120,6 @@ export interface SessionManagerAPI {
1072
1120
  ) => Promise<AgentMemoryMutationResult>;
1073
1121
  /** Delete one entry. Removes the file outright when last entry is removed. */
1074
1122
  delete: (agentId: string, entryId: string) => Promise<AgentMemoryMutationResult>;
1075
- /** List all agents that currently have a memory file on disk. */
1076
- listAgents: () => Promise<AgentMemoryListAgentsResult>;
1077
1123
  };
1078
1124
  docEditor: {
1079
1125
  pickFile: (payload?: { lastDir?: string }) => Promise<{ path: string | null; error?: string }>;
@@ -1121,88 +1167,48 @@ export interface SessionManagerAPI {
1121
1167
  * E2E session as authenticated and unblocks MUTATE-tier commands. */
1122
1168
  confirmSas: () => Promise<WebRemoteMutationResult>;
1123
1169
  };
1124
- kg: {
1125
- /** Distilled knowledge graph + ingest status for ONE project (`cwd`).
1126
- * Omit `cwd` for the most-active project. */
1127
- get: (cwd?: string) => Promise<KgState>;
1128
- /** Projects seen in the prompt log, with per-project graph stats. */
1129
- projects: () => Promise<KgProject[]>;
1130
- /** Process new prompt-log lines into per-project graphs (incremental, global). */
1131
- ingest: () => Promise<{ ok: boolean; added?: number; projects?: number; stopped?: boolean; error?: string; note?: string }>;
1132
- /** Ask a question answered from ONE project's graph + prompts via claude -p. */
1133
- ask: (question: string, cwd?: string) => Promise<{ ok: boolean; answer?: string; cited?: { ts: string; prompt: string }[]; error?: string }>;
1134
- /** Purge graphs: one project (`{ cwd }`) or all (`{ all: true }`). */
1135
- clear: (arg?: { cwd?: string; all?: boolean }) => Promise<{ ok: boolean; cleared?: string; removed?: number }>;
1136
- /** Toggle the recurring claude -p extraction on/off (sets captureMode 'llm'/'off'). */
1137
- setExtraction: (enabled: boolean) => Promise<{ ok: boolean; extractionEnabled: boolean }>;
1138
- /** Set capture mode: 'llm' = claude -p extraction; 'lite' = heuristic (free); 'off' = disabled. */
1139
- setCaptureMode: (mode: 'llm' | 'lite' | 'off') => Promise<{ ok: boolean; captureMode?: string; error?: string }>;
1140
- onIngestProgress: (handler: (ev: KgIngestProgress) => void) => () => void;
1170
+ chat: {
1171
+ /** Spawn a headless `claude -p` run for a tab. Results arrive via the on* listeners. */
1172
+ run: (payload: ChatRunPayload) => Promise<{ ok: boolean }>;
1173
+ /** Cancel the in-flight run for a tab (SIGTERM→SIGKILL). No-op when idle. */
1174
+ cancel: (tabId: string) => void;
1175
+ onQueued: (handler: (e: ChatRunQueuedEvent) => void) => () => void;
1176
+ onRunStarted: (handler: (e: ChatRunStartedEvent) => void) => () => void;
1177
+ onOutput: (handler: (e: ChatRunOutputEvent) => void) => () => void;
1178
+ onNeedsInput: (handler: (e: ChatRunNeedsInputEvent) => void) => () => void;
1179
+ onComplete: (handler: (e: ChatRunCompleteEvent) => void) => () => void;
1180
+ onError: (handler: (e: ChatRunErrorEvent) => void) => () => void;
1181
+ };
1182
+ exchanges: {
1183
+ /** Durable per-exchange log entries for a project, newest-first (max 100 by default). */
1184
+ list: (payload: {
1185
+ cwd: string;
1186
+ sessionId?: string;
1187
+ limit?: number;
1188
+ offset?: number;
1189
+ }) => Promise<Exchange[]>;
1141
1190
  };
1142
1191
  }
1143
1192
 
1144
- export interface KgProject {
1145
- cwd: string;
1146
- /** Last path segment of `cwd`, for display. */
1147
- label: string;
1148
- /** Prompts logged for this project (real prompts only). */
1149
- total: number;
1150
- /** Prompts already distilled into this project's graph. */
1151
- processed: number;
1152
- /** total - processed. */
1153
- pending: number;
1154
- nodes: number;
1155
- edges: number;
1156
- lastIngest: string | null;
1157
- }
1158
-
1159
- export interface KgNode {
1160
- id: string;
1161
- key: string;
1162
- name: string;
1163
- type: string;
1164
- description: string;
1165
- count: number;
1166
- firstTs: string | null;
1167
- lastTs: string | null;
1168
- }
1169
- export interface KgEdge {
1170
- src: string;
1171
- dst: string;
1172
- relation: string;
1173
- weight: number;
1174
- lastTs: string | null;
1175
- }
1176
- export interface KgState {
1177
- /** The project this graph belongs to. */
1193
+ // ────────────────────────────────────────────── Exchanges (PRD 324 read path)
1194
+
1195
+ export interface Exchange {
1196
+ /** ISO 8601 timestamp when the exchange was recorded. */
1197
+ ts: string;
1198
+ /** Claude session UUID. */
1199
+ sessionId: string;
1200
+ /** Project cwd. */
1178
1201
  cwd: string;
1179
- /** Last path segment of `cwd`, for display. */
1180
- label: string;
1181
- nodes: KgNode[];
1182
- edges: KgEdge[];
1183
- status: {
1184
- promptCount: number;
1185
- totalPrompts: number;
1186
- pending: number;
1187
- lastIngest: string | null;
1188
- ingesting: boolean;
1189
- logPath: string;
1190
- extractionEnabled: boolean;
1191
- /** Active capture mode: 'llm' | 'lite' | 'off'. */
1192
- captureMode: 'llm' | 'lite' | 'off';
1193
- /** Node cap from kg-config.json; 0 = disabled; default 300. */
1194
- maxGraphNodes: number;
1195
- };
1196
- }
1197
- export interface KgIngestProgress {
1198
- phase: 'start' | 'extract' | 'batch' | 'done' | 'error';
1199
- ingesting: boolean;
1200
- batch?: number;
1201
- totalBatches?: number;
1202
- added?: number;
1203
- /** Set on `batch`: the project cwd whose graph just committed. */
1204
- cwd?: string;
1205
- error?: string;
1202
+ /** The user prompt that started the exchange. */
1203
+ prompt: string;
1204
+ /** The verbatim assistant result. */
1205
+ result: string;
1206
+ /** Haiku-generated one-sentence summary. */
1207
+ summary: string;
1208
+ /** Haiku model used for the summary (informational). */
1209
+ model?: string;
1210
+ /** Set when summarization failed — `result` is available but `summary` may be empty. */
1211
+ degraded?: boolean;
1206
1212
  }
1207
1213
 
1208
1214
  declare global {