icopilot 2.2.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 (203) hide show
  1. package/CHANGELOG.md +250 -0
  2. package/LICENSE +21 -0
  3. package/README.md +214 -0
  4. package/bin/icopilot.js +6 -0
  5. package/dist/acp/router.js +123 -0
  6. package/dist/acp/schema.js +53 -0
  7. package/dist/agents/aggregator.js +187 -0
  8. package/dist/agents/custom-agents.js +97 -0
  9. package/dist/agents/goal-driven.js +411 -0
  10. package/dist/agents/multi-repo.js +350 -0
  11. package/dist/agents/parallel-runner.js +181 -0
  12. package/dist/agents/router.js +144 -0
  13. package/dist/agents/self-heal.js +481 -0
  14. package/dist/agents/tdd-agent.js +278 -0
  15. package/dist/api/github-models.js +158 -0
  16. package/dist/bridge/ide-bridge.js +479 -0
  17. package/dist/cloud/routine-executor.js +34 -0
  18. package/dist/cloud/routine-scheduler.js +67 -0
  19. package/dist/cloud/routine-storage.js +297 -0
  20. package/dist/commands/acp-cmd.js +143 -0
  21. package/dist/commands/actions-cmd.js +624 -0
  22. package/dist/commands/agent-cmd.js +144 -0
  23. package/dist/commands/alias-cmd.js +132 -0
  24. package/dist/commands/bookmark-cmd.js +77 -0
  25. package/dist/commands/changelog-cmd.js +99 -0
  26. package/dist/commands/changes-cmd.js +120 -0
  27. package/dist/commands/clipboard-cmd.js +217 -0
  28. package/dist/commands/cloud-routine-cmd.js +265 -0
  29. package/dist/commands/codegen-cmd.js +544 -0
  30. package/dist/commands/compare-cmd.js +116 -0
  31. package/dist/commands/context-cmd.js +247 -0
  32. package/dist/commands/context-viz-cmd.js +43 -0
  33. package/dist/commands/conventions-cmd.js +116 -0
  34. package/dist/commands/cost-cmd.js +51 -0
  35. package/dist/commands/deps-cmd.js +294 -0
  36. package/dist/commands/diagram-cmd.js +658 -0
  37. package/dist/commands/diff-review-cmd.js +92 -0
  38. package/dist/commands/doc-cmd.js +412 -0
  39. package/dist/commands/doctor-cmd.js +152 -0
  40. package/dist/commands/editor-cmd.js +49 -0
  41. package/dist/commands/env-cmd.js +86 -0
  42. package/dist/commands/explain-cmd.js +78 -0
  43. package/dist/commands/explain-shell-cmd.js +22 -0
  44. package/dist/commands/explore-cmd.js +231 -0
  45. package/dist/commands/feedback-cmd.js +98 -0
  46. package/dist/commands/fix-cmd.js +17 -0
  47. package/dist/commands/generate-cmd.js +38 -0
  48. package/dist/commands/git-extra.js +197 -0
  49. package/dist/commands/git-log-cmd.js +98 -0
  50. package/dist/commands/git-undo-cmd.js +137 -0
  51. package/dist/commands/git.js +155 -0
  52. package/dist/commands/history-cmd.js +122 -0
  53. package/dist/commands/index-cmd.js +65 -0
  54. package/dist/commands/init-cmd.js +73 -0
  55. package/dist/commands/lint-cmd.js +133 -0
  56. package/dist/commands/memory-cmd.js +98 -0
  57. package/dist/commands/metrics-cmd.js +97 -0
  58. package/dist/commands/mode-prefix.js +30 -0
  59. package/dist/commands/multi-cmd.js +44 -0
  60. package/dist/commands/notify-cmd.js +204 -0
  61. package/dist/commands/profile-cmd.js +101 -0
  62. package/dist/commands/prompts.js +17 -0
  63. package/dist/commands/rag-cmd.js +60 -0
  64. package/dist/commands/readme-cmd.js +564 -0
  65. package/dist/commands/reasoning-cmd.js +34 -0
  66. package/dist/commands/refactor-cmd.js +96 -0
  67. package/dist/commands/release-cmd.js +450 -0
  68. package/dist/commands/repo-cmd.js +195 -0
  69. package/dist/commands/route-cmd.js +21 -0
  70. package/dist/commands/schedule-cmd.js +109 -0
  71. package/dist/commands/search-cmd.js +47 -0
  72. package/dist/commands/security-cmd.js +156 -0
  73. package/dist/commands/settings-cmd.js +238 -0
  74. package/dist/commands/skill-cmd.js +338 -0
  75. package/dist/commands/slash.js +2721 -0
  76. package/dist/commands/snippets-cmd.js +83 -0
  77. package/dist/commands/space-cmd.js +92 -0
  78. package/dist/commands/stash-cmd.js +156 -0
  79. package/dist/commands/stats-cmd.js +36 -0
  80. package/dist/commands/style-cmd.js +85 -0
  81. package/dist/commands/suggest-cmd.js +40 -0
  82. package/dist/commands/summary-cmd.js +138 -0
  83. package/dist/commands/task-cmd.js +58 -0
  84. package/dist/commands/team-memory-cmd.js +97 -0
  85. package/dist/commands/template-cmd.js +475 -0
  86. package/dist/commands/test-cmd.js +146 -0
  87. package/dist/commands/todo-cmd.js +172 -0
  88. package/dist/commands/tokens-cmd.js +277 -0
  89. package/dist/commands/trigger-cmd.js +147 -0
  90. package/dist/commands/undo-cmd.js +18 -0
  91. package/dist/commands/voice-cmd.js +89 -0
  92. package/dist/commands/watch-cmd.js +110 -0
  93. package/dist/commands/web-cmd.js +183 -0
  94. package/dist/commands/worktree-cmd.js +119 -0
  95. package/dist/config-profile.js +66 -0
  96. package/dist/config.js +288 -0
  97. package/dist/context/compactor.js +53 -0
  98. package/dist/context/dep-context.js +329 -0
  99. package/dist/context/file-refs.js +54 -0
  100. package/dist/context/git-context.js +229 -0
  101. package/dist/context/image-input.js +66 -0
  102. package/dist/context/memory.js +55 -0
  103. package/dist/context/persistent-memory.js +104 -0
  104. package/dist/context/pinned.js +96 -0
  105. package/dist/context/priority.js +150 -0
  106. package/dist/context/read-only.js +48 -0
  107. package/dist/context/smart-files.js +286 -0
  108. package/dist/context/team-memory.js +156 -0
  109. package/dist/extensions/loader.js +149 -0
  110. package/dist/extensions/marketplace.js +49 -0
  111. package/dist/extensions/slack-provider.js +181 -0
  112. package/dist/extensions/team.js +56 -0
  113. package/dist/extensions/teams-provider.js +222 -0
  114. package/dist/extensions/voice.js +18 -0
  115. package/dist/hooks/lifecycle.js +215 -0
  116. package/dist/hooks/precommit.js +463 -0
  117. package/dist/index/embeddings.js +23 -0
  118. package/dist/index/indexer.js +86 -0
  119. package/dist/index/retrieve.js +20 -0
  120. package/dist/index/store.js +95 -0
  121. package/dist/index.js +286 -0
  122. package/dist/intelligence/dead-code.js +457 -0
  123. package/dist/intelligence/error-watch.js +263 -0
  124. package/dist/intelligence/navigation.js +141 -0
  125. package/dist/intelligence/stack-trace.js +210 -0
  126. package/dist/intelligence/symbol-index.js +410 -0
  127. package/dist/knowledge/auto-memory.js +412 -0
  128. package/dist/knowledge/conventions.js +475 -0
  129. package/dist/knowledge/corrections.js +213 -0
  130. package/dist/knowledge/rag.js +450 -0
  131. package/dist/knowledge/style-learner.js +324 -0
  132. package/dist/logger.js +35 -0
  133. package/dist/mcp/client.js +144 -0
  134. package/dist/mcp/config.js +24 -0
  135. package/dist/mcp/index.js +89 -0
  136. package/dist/modes/auto-compact.js +20 -0
  137. package/dist/modes/autopilot.js +157 -0
  138. package/dist/modes/background.js +82 -0
  139. package/dist/modes/interactive.js +187 -0
  140. package/dist/modes/oneshot.js +36 -0
  141. package/dist/modes/tui.js +265 -0
  142. package/dist/modes/turn.js +342 -0
  143. package/dist/notifications/manager.js +107 -0
  144. package/dist/plugins/marketplace.js +244 -0
  145. package/dist/providers/custom-provider.js +298 -0
  146. package/dist/providers/local-model.js +121 -0
  147. package/dist/routing/profiles.js +44 -0
  148. package/dist/routing/router.js +18 -0
  149. package/dist/sandbox/container.js +151 -0
  150. package/dist/security/audit.js +237 -0
  151. package/dist/security/content-filter.js +449 -0
  152. package/dist/security/proxy.js +301 -0
  153. package/dist/security/retention.js +281 -0
  154. package/dist/security/roles.js +252 -0
  155. package/dist/server/api-server.js +679 -0
  156. package/dist/session/bookmarks.js +72 -0
  157. package/dist/session/cloud-session.js +291 -0
  158. package/dist/session/handoff.js +405 -0
  159. package/dist/session/manager.js +35 -0
  160. package/dist/session/session.js +296 -0
  161. package/dist/session/share.js +313 -0
  162. package/dist/session/undo-journal.js +91 -0
  163. package/dist/snippets/store.js +60 -0
  164. package/dist/spaces/space-config.js +156 -0
  165. package/dist/spaces/space.js +220 -0
  166. package/dist/stats/store.js +101 -0
  167. package/dist/tools/apply-patch.js +134 -0
  168. package/dist/tools/auto-check.js +218 -0
  169. package/dist/tools/diff-edit.js +150 -0
  170. package/dist/tools/diff-prompt.js +36 -0
  171. package/dist/tools/edit-file.js +66 -0
  172. package/dist/tools/file-ops.js +205 -0
  173. package/dist/tools/glob.js +17 -0
  174. package/dist/tools/grep.js +56 -0
  175. package/dist/tools/image.js +194 -0
  176. package/dist/tools/list-directory.js +228 -0
  177. package/dist/tools/memory.js +17 -0
  178. package/dist/tools/multi-edit.js +299 -0
  179. package/dist/tools/policy.js +95 -0
  180. package/dist/tools/registry.js +484 -0
  181. package/dist/tools/retry.js +74 -0
  182. package/dist/tools/run-in-terminal.js +162 -0
  183. package/dist/tools/safety.js +64 -0
  184. package/dist/tools/sandbox.js +15 -0
  185. package/dist/tools/search-symbols.js +212 -0
  186. package/dist/tools/shell.js +118 -0
  187. package/dist/tools/web.js +167 -0
  188. package/dist/ui/prompt.js +37 -0
  189. package/dist/ui/render.js +96 -0
  190. package/dist/ui/screen.js +13 -0
  191. package/dist/ui/theme.js +56 -0
  192. package/dist/util/browser.js +34 -0
  193. package/dist/util/completion.js +350 -0
  194. package/dist/util/cost.js +28 -0
  195. package/dist/util/keybindings.js +113 -0
  196. package/dist/util/lazy.js +26 -0
  197. package/dist/util/perf.js +25 -0
  198. package/dist/util/token-worker.js +11 -0
  199. package/dist/util/tokens.js +50 -0
  200. package/dist/workflows/builtins.js +128 -0
  201. package/dist/workflows/engine.js +496 -0
  202. package/dist/workflows/file-trigger.js +197 -0
  203. package/package.json +79 -0
@@ -0,0 +1,405 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import { PersistentMemory } from '../context/persistent-memory.js';
5
+ import { Session } from './session.js';
6
+ const HANDOFF_VERSION = 1;
7
+ const DEFAULT_FILENAME = '.icopilot-handoff.json';
8
+ export function createHandoff(session, opts = {}) {
9
+ const pinned = clonePinned(session.state.pinned);
10
+ const memory = loadPersistentMemory(session.state.cwd, opts.memoryStorePath);
11
+ const files = pinned.map((file) => snapshotPinnedFile(file));
12
+ const messages = limitMessages(session.state.messages, opts.maxMessages);
13
+ return {
14
+ version: HANDOFF_VERSION,
15
+ session: {
16
+ id: session.state.id,
17
+ createdAt: session.state.createdAt,
18
+ model: session.state.model,
19
+ mode: session.state.mode,
20
+ cwd: session.state.cwd,
21
+ messages: cloneMessages(messages),
22
+ todos: cloneTodos(session.state.todos),
23
+ autopilotEnabled: Boolean(session.state.autopilotEnabled),
24
+ systemPrompt: typeof session.state.systemPrompt === 'string' ? session.state.systemPrompt : undefined,
25
+ pinned,
26
+ gitContext: cloneGitContext(session.state.gitContext),
27
+ },
28
+ context: {
29
+ files,
30
+ pinned,
31
+ memory,
32
+ },
33
+ metadata: {
34
+ author: opts.author?.trim() || detectAuthor(),
35
+ timestamp: new Date().toISOString(),
36
+ branch: opts.branch?.trim() || detectBranch(session.state.cwd),
37
+ description: opts.description?.trim() || '',
38
+ },
39
+ };
40
+ }
41
+ export function receiveHandoff(bundle) {
42
+ const normalized = validateHandoffBundle(bundle);
43
+ const importedContext = buildImportedContextMessage(normalized);
44
+ const session = new Session({
45
+ createdAt: normalized.session.createdAt,
46
+ model: normalized.session.model,
47
+ mode: normalized.session.mode,
48
+ cwd: normalized.session.cwd,
49
+ messages: importedContext
50
+ ? [
51
+ { role: 'system', content: importedContext },
52
+ ...cloneMessages(normalized.session.messages),
53
+ ]
54
+ : cloneMessages(normalized.session.messages),
55
+ todos: cloneTodos(normalized.session.todos),
56
+ autopilotEnabled: Boolean(normalized.session.autopilotEnabled),
57
+ systemPrompt: typeof normalized.session.systemPrompt === 'string'
58
+ ? normalized.session.systemPrompt
59
+ : undefined,
60
+ pinned: clonePinned(normalized.context.pinned),
61
+ gitContext: cloneGitContext(normalized.session.gitContext),
62
+ });
63
+ restorePersistentMemory(session.state.cwd, normalized.context.memory);
64
+ session.persist();
65
+ return session;
66
+ }
67
+ export function exportHandoffFile(bundle, targetPath) {
68
+ const normalized = validateHandoffBundle(bundle);
69
+ const target = resolveHandoffPath(normalized.session.cwd, targetPath);
70
+ fs.mkdirSync(path.dirname(target), { recursive: true });
71
+ fs.writeFileSync(target, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
72
+ return target;
73
+ }
74
+ export function importHandoffFile(targetPath) {
75
+ const resolved = path.resolve(targetPath);
76
+ const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
77
+ return validateHandoffBundle(parsed);
78
+ }
79
+ export function previewHandoff(bundle) {
80
+ const normalized = validateHandoffBundle(bundle);
81
+ const previewFiles = normalized.context.files.map((file) => file.missing ? `${file.path} (missing snapshot)` : file.path);
82
+ const lines = [
83
+ 'Handoff bundle',
84
+ ` version: ${normalized.version}`,
85
+ ` author: ${normalized.metadata.author}`,
86
+ ` timestamp: ${normalized.metadata.timestamp}`,
87
+ ` branch: ${normalized.metadata.branch || '(unknown)'}`,
88
+ ` description: ${normalized.metadata.description || '(none)'}`,
89
+ ` cwd: ${normalized.session.cwd}`,
90
+ ` model: ${normalized.session.model}`,
91
+ ` mode: ${normalized.session.mode}`,
92
+ ` messages: ${normalized.session.messages.length}`,
93
+ ` todos: ${normalized.session.todos.length}`,
94
+ ` pinned: ${normalized.context.pinned.length}`,
95
+ ` memory: ${normalized.context.memory.length}`,
96
+ ];
97
+ if (previewFiles.length) {
98
+ lines.push(' files:');
99
+ previewFiles.forEach((file) => lines.push(` - ${file}`));
100
+ }
101
+ return `${lines.join('\n')}\n`;
102
+ }
103
+ function validateHandoffBundle(input) {
104
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
105
+ throw new Error('Handoff bundle must be a JSON object.');
106
+ }
107
+ const bundle = input;
108
+ if (bundle.version !== HANDOFF_VERSION) {
109
+ throw new Error(`Unsupported handoff bundle version: ${typeof bundle.version === 'number' ? bundle.version : 'unknown'}.`);
110
+ }
111
+ return {
112
+ version: HANDOFF_VERSION,
113
+ session: validateSessionState(bundle.session),
114
+ context: validateContext(bundle.context),
115
+ metadata: validateMetadata(bundle.metadata),
116
+ };
117
+ }
118
+ function validateSessionState(input) {
119
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
120
+ throw new Error('Handoff bundle session is missing.');
121
+ }
122
+ const session = input;
123
+ const mode = session.mode === 'plan' ? 'plan' : 'ask';
124
+ if (typeof session.id !== 'string' || !session.id.trim()) {
125
+ throw new Error('Handoff bundle session id is missing.');
126
+ }
127
+ if (typeof session.createdAt !== 'string' || !session.createdAt.trim()) {
128
+ throw new Error('Handoff bundle session createdAt is missing.');
129
+ }
130
+ if (typeof session.model !== 'string' || !session.model.trim()) {
131
+ throw new Error('Handoff bundle session model is missing.');
132
+ }
133
+ if (typeof session.cwd !== 'string' || !session.cwd.trim()) {
134
+ throw new Error('Handoff bundle session cwd is missing.');
135
+ }
136
+ if (!Array.isArray(session.messages)) {
137
+ throw new Error('Handoff bundle session messages must be an array.');
138
+ }
139
+ return {
140
+ id: session.id,
141
+ createdAt: session.createdAt,
142
+ model: session.model,
143
+ mode,
144
+ cwd: session.cwd,
145
+ messages: cloneMessages(session.messages),
146
+ todos: cloneTodos(session.todos),
147
+ autopilotEnabled: Boolean(session.autopilotEnabled),
148
+ systemPrompt: typeof session.systemPrompt === 'string' ? session.systemPrompt : undefined,
149
+ pinned: clonePinned(session.pinned),
150
+ gitContext: cloneGitContext(session.gitContext),
151
+ };
152
+ }
153
+ function validateContext(input) {
154
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
155
+ throw new Error('Handoff bundle context is missing.');
156
+ }
157
+ const context = input;
158
+ if (!Array.isArray(context.files)) {
159
+ throw new Error('Handoff bundle context files must be an array.');
160
+ }
161
+ if (!Array.isArray(context.pinned)) {
162
+ throw new Error('Handoff bundle context pinned must be an array.');
163
+ }
164
+ if (!Array.isArray(context.memory)) {
165
+ throw new Error('Handoff bundle context memory must be an array.');
166
+ }
167
+ return {
168
+ files: context.files.flatMap((file) => {
169
+ if (!file || typeof file !== 'object')
170
+ return [];
171
+ const candidate = file;
172
+ if (typeof candidate.path !== 'string' || typeof candidate.content !== 'string')
173
+ return [];
174
+ return [
175
+ {
176
+ path: candidate.path,
177
+ content: candidate.content,
178
+ addedAt: typeof candidate.addedAt === 'string' ? candidate.addedAt : undefined,
179
+ tokens: typeof candidate.tokens === 'number' ? candidate.tokens : undefined,
180
+ missing: Boolean(candidate.missing),
181
+ },
182
+ ];
183
+ }),
184
+ pinned: clonePinned(context.pinned),
185
+ memory: cloneMemoryEntries(context.memory),
186
+ };
187
+ }
188
+ function validateMetadata(input) {
189
+ if (!input || typeof input !== 'object' || Array.isArray(input)) {
190
+ throw new Error('Handoff bundle metadata is missing.');
191
+ }
192
+ const metadata = input;
193
+ if (typeof metadata.author !== 'string' || !metadata.author.trim()) {
194
+ throw new Error('Handoff bundle metadata author is missing.');
195
+ }
196
+ if (typeof metadata.timestamp !== 'string' || !metadata.timestamp.trim()) {
197
+ throw new Error('Handoff bundle metadata timestamp is missing.');
198
+ }
199
+ if (typeof metadata.branch !== 'string') {
200
+ throw new Error('Handoff bundle metadata branch is invalid.');
201
+ }
202
+ if (typeof metadata.description !== 'string') {
203
+ throw new Error('Handoff bundle metadata description is invalid.');
204
+ }
205
+ return {
206
+ author: metadata.author,
207
+ timestamp: metadata.timestamp,
208
+ branch: metadata.branch,
209
+ description: metadata.description,
210
+ };
211
+ }
212
+ function limitMessages(messages, maxMessages) {
213
+ if (typeof maxMessages !== 'number' || !Number.isFinite(maxMessages) || maxMessages <= 0) {
214
+ return cloneMessages(messages);
215
+ }
216
+ return cloneMessages(messages.slice(-Math.floor(maxMessages)));
217
+ }
218
+ function snapshotPinnedFile(file) {
219
+ try {
220
+ return {
221
+ path: file.path,
222
+ addedAt: file.addedAt,
223
+ tokens: file.tokens,
224
+ content: fs.readFileSync(file.path, 'utf8'),
225
+ missing: false,
226
+ };
227
+ }
228
+ catch {
229
+ return {
230
+ path: file.path,
231
+ addedAt: file.addedAt,
232
+ tokens: file.tokens,
233
+ content: '',
234
+ missing: true,
235
+ };
236
+ }
237
+ }
238
+ function loadPersistentMemory(cwd, storePath) {
239
+ const memory = new PersistentMemory(storePath);
240
+ memory.load(memory.getProjectId(cwd));
241
+ return cloneMemoryEntries(memory.recall());
242
+ }
243
+ function restorePersistentMemory(cwd, entries) {
244
+ const memory = new PersistentMemory();
245
+ const projectId = memory.getProjectId(cwd);
246
+ for (const entry of entries) {
247
+ memory.remember(entry.key, entry.value, entry.source);
248
+ }
249
+ memory.save(projectId);
250
+ }
251
+ function buildImportedContextMessage(bundle) {
252
+ const lines = [
253
+ 'Imported handoff bundle context.',
254
+ `Author: ${bundle.metadata.author}`,
255
+ `Timestamp: ${bundle.metadata.timestamp}`,
256
+ `Branch: ${bundle.metadata.branch || '(unknown)'}`,
257
+ ];
258
+ if (bundle.metadata.description) {
259
+ lines.push(`Description: ${bundle.metadata.description}`);
260
+ }
261
+ if (bundle.context.memory.length) {
262
+ lines.push('', 'Persistent memory:');
263
+ for (const entry of bundle.context.memory) {
264
+ lines.push(`- ${entry.key}: ${entry.value} (${entry.source}, ${entry.addedAt})`);
265
+ }
266
+ }
267
+ if (bundle.context.files.length) {
268
+ lines.push('', 'Pinned file snapshots:');
269
+ for (const file of bundle.context.files) {
270
+ lines.push('', `File: ${file.path}`);
271
+ if (file.missing) {
272
+ lines.push('[snapshot unavailable]');
273
+ continue;
274
+ }
275
+ lines.push('```');
276
+ lines.push(file.content);
277
+ lines.push('```');
278
+ }
279
+ }
280
+ return lines.join('\n');
281
+ }
282
+ function detectAuthor() {
283
+ return (process.env.GIT_AUTHOR_NAME?.trim() ||
284
+ process.env.GIT_COMMITTER_NAME?.trim() ||
285
+ process.env.USERNAME?.trim() ||
286
+ process.env.USER?.trim() ||
287
+ 'unknown');
288
+ }
289
+ function detectBranch(cwd) {
290
+ try {
291
+ return execSync('git rev-parse --abbrev-ref HEAD', {
292
+ cwd,
293
+ encoding: 'utf8',
294
+ stdio: ['ignore', 'pipe', 'ignore'],
295
+ }).trim();
296
+ }
297
+ catch {
298
+ return '';
299
+ }
300
+ }
301
+ function resolveHandoffPath(cwd, targetPath) {
302
+ if (!targetPath?.trim())
303
+ return path.resolve(cwd, DEFAULT_FILENAME);
304
+ const resolved = path.resolve(cwd, targetPath.trim());
305
+ try {
306
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
307
+ return path.join(resolved, DEFAULT_FILENAME);
308
+ }
309
+ }
310
+ catch {
311
+ /* ignore stat errors */
312
+ }
313
+ return resolved;
314
+ }
315
+ function cloneMessages(messages) {
316
+ if (!Array.isArray(messages))
317
+ return [];
318
+ return JSON.parse(JSON.stringify(messages));
319
+ }
320
+ function cloneTodos(todos) {
321
+ if (!Array.isArray(todos))
322
+ return [];
323
+ return todos.flatMap((todo) => {
324
+ if (!todo || typeof todo !== 'object')
325
+ return [];
326
+ const candidate = todo;
327
+ if (typeof candidate.id !== 'string' ||
328
+ typeof candidate.text !== 'string' ||
329
+ typeof candidate.done !== 'boolean' ||
330
+ typeof candidate.createdAt !== 'string') {
331
+ return [];
332
+ }
333
+ return [
334
+ {
335
+ id: candidate.id,
336
+ text: candidate.text,
337
+ done: candidate.done,
338
+ createdAt: candidate.createdAt,
339
+ completedAt: typeof candidate.completedAt === 'string' ? candidate.completedAt : undefined,
340
+ },
341
+ ];
342
+ });
343
+ }
344
+ function clonePinned(files) {
345
+ if (!Array.isArray(files))
346
+ return [];
347
+ return files.flatMap((file) => {
348
+ if (!file || typeof file !== 'object')
349
+ return [];
350
+ const candidate = file;
351
+ if (typeof candidate.path !== 'string' ||
352
+ typeof candidate.addedAt !== 'string' ||
353
+ typeof candidate.tokens !== 'number') {
354
+ return [];
355
+ }
356
+ return [
357
+ {
358
+ path: candidate.path,
359
+ addedAt: candidate.addedAt,
360
+ tokens: candidate.tokens,
361
+ },
362
+ ];
363
+ });
364
+ }
365
+ function cloneGitContext(files) {
366
+ if (!Array.isArray(files))
367
+ return [];
368
+ return files.flatMap((file) => {
369
+ if (!file || typeof file !== 'object')
370
+ return [];
371
+ const candidate = file;
372
+ if (typeof candidate.path !== 'string' || typeof candidate.status !== 'string') {
373
+ return [];
374
+ }
375
+ return [
376
+ {
377
+ path: candidate.path,
378
+ status: candidate.status,
379
+ },
380
+ ];
381
+ });
382
+ }
383
+ function cloneMemoryEntries(entries) {
384
+ if (!Array.isArray(entries))
385
+ return [];
386
+ return entries.flatMap((entry) => {
387
+ if (!entry || typeof entry !== 'object')
388
+ return [];
389
+ const candidate = entry;
390
+ if (typeof candidate.key !== 'string' ||
391
+ typeof candidate.value !== 'string' ||
392
+ typeof candidate.addedAt !== 'string' ||
393
+ (candidate.source !== 'user' && candidate.source !== 'auto')) {
394
+ return [];
395
+ }
396
+ return [
397
+ {
398
+ key: candidate.key,
399
+ value: candidate.value,
400
+ addedAt: candidate.addedAt,
401
+ source: candidate.source,
402
+ },
403
+ ];
404
+ });
405
+ }
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { select } from '@inquirer/prompts';
4
+ import { Session } from './session.js';
5
+ export async function pickSession() {
6
+ const sessions = Session.list();
7
+ if (!sessions.length)
8
+ return null;
9
+ return select({
10
+ message: 'Resume session',
11
+ choices: sessions.map((s) => ({
12
+ name: `${s.id.slice(0, 8)} ${s.model} ${s.messageCount} msgs ${age(s.mtime)}`,
13
+ value: s.id,
14
+ description: s.file,
15
+ })),
16
+ }).catch(() => null);
17
+ }
18
+ export async function exportSession(session, format, outPath) {
19
+ const target = path.resolve(session.state.cwd, outPath || `session-${session.state.id}.${format}`);
20
+ await fs.mkdir(path.dirname(target), { recursive: true });
21
+ await fs.writeFile(target, format === 'json' ? session.toJSON() + '\n' : session.toMarkdown(), 'utf8');
22
+ return target;
23
+ }
24
+ function age(date) {
25
+ const seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));
26
+ if (seconds < 60)
27
+ return `${seconds}s ago`;
28
+ const minutes = Math.floor(seconds / 60);
29
+ if (minutes < 60)
30
+ return `${minutes}m ago`;
31
+ const hours = Math.floor(minutes / 60);
32
+ if (hours < 48)
33
+ return `${hours}h ago`;
34
+ return `${Math.floor(hours / 24)}d ago`;
35
+ }