@keyou007/agent-bot 0.1.20-alpha.1 → 0.1.21

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 (92) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +27 -4
  3. package/README.zh.md +23 -4
  4. package/dist/cli/AgentPrerequisites.js +11 -47
  5. package/dist/cli/AgentPrerequisites.js.map +1 -1
  6. package/dist/cli/DefaultAgentSelection.js +1 -1
  7. package/dist/cli/DefaultAgentSelection.js.map +1 -1
  8. package/dist/cli.js +15 -0
  9. package/dist/cli.js.map +1 -1
  10. package/dist/codex/CodexEventMapper.js +21 -1
  11. package/dist/codex/CodexEventMapper.js.map +1 -1
  12. package/dist/codex/CodexLocalActivityDetector.js +32 -10
  13. package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
  14. package/dist/codex/CodexProcessManager.js +56 -7
  15. package/dist/codex/CodexProcessManager.js.map +1 -1
  16. package/dist/codex/CodexRuntime.js +361 -168
  17. package/dist/codex/CodexRuntime.js.map +1 -1
  18. package/dist/codex/CodexVersion.js +18 -0
  19. package/dist/codex/CodexVersion.js.map +1 -0
  20. package/dist/codex/ProviderModels.js +159 -0
  21. package/dist/codex/ProviderModels.js.map +1 -0
  22. package/dist/codex/ProviderSwitch.js +32 -0
  23. package/dist/codex/ProviderSwitch.js.map +1 -0
  24. package/dist/commands/CommandRouter.js +8 -3
  25. package/dist/commands/CommandRouter.js.map +1 -1
  26. package/dist/console/ConsoleTurnPresenter.js +2 -1
  27. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  28. package/dist/feishu/CardRenderer.js +82 -27
  29. package/dist/feishu/CardRenderer.js.map +1 -1
  30. package/dist/feishu/FeishuConnector.js +17 -5
  31. package/dist/feishu/FeishuConnector.js.map +1 -1
  32. package/dist/feishu/FeishuTurnPresenter.js +34 -8
  33. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  34. package/dist/feishu/InboundText.js +11 -0
  35. package/dist/feishu/InboundText.js.map +1 -1
  36. package/dist/feishu/MergedForwardMessage.js +17 -5
  37. package/dist/feishu/MergedForwardMessage.js.map +1 -1
  38. package/dist/index.js +3 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/local-files/LocalFileViewerServer.js +164 -19
  41. package/dist/local-files/LocalFileViewerServer.js.map +1 -1
  42. package/dist/local-files/MarkdownFileLinks.js +65 -0
  43. package/dist/local-files/MarkdownFileLinks.js.map +1 -0
  44. package/dist/local-files/TurnPreviewPage.js +725 -0
  45. package/dist/local-files/TurnPreviewPage.js.map +1 -0
  46. package/dist/presentation/OutboundRouter.js +6 -1
  47. package/dist/presentation/OutboundRouter.js.map +1 -1
  48. package/dist/presentation/TurnStateReducer.js +142 -3
  49. package/dist/presentation/TurnStateReducer.js.map +1 -1
  50. package/dist/proxy/ProxySessionController.js +573 -325
  51. package/dist/proxy/ProxySessionController.js.map +1 -1
  52. package/dist/state/StateStore.js +65 -65
  53. package/dist/state/StateStore.js.map +1 -1
  54. package/dist/utils/formatStorageSize.js +15 -0
  55. package/dist/utils/formatStorageSize.js.map +1 -0
  56. package/dist/utils/semanticVersion.js +39 -0
  57. package/dist/utils/semanticVersion.js.map +1 -0
  58. package/docs/technical-reference.md +11 -3
  59. package/docs/technical-reference.zh.md +11 -3
  60. package/npm-shrinkwrap.json +2 -2
  61. package/package.json +1 -1
  62. package/skills/agent-bot/SKILL.md +11 -1
  63. package/src/cli/AgentPrerequisites.ts +14 -40
  64. package/src/cli/DefaultAgentSelection.ts +1 -1
  65. package/src/cli.ts +13 -0
  66. package/src/codex/CodexEventMapper.ts +31 -1
  67. package/src/codex/CodexLocalActivityDetector.ts +41 -9
  68. package/src/codex/CodexProcessManager.ts +48 -7
  69. package/src/codex/CodexRuntime.ts +397 -204
  70. package/src/codex/CodexVersion.ts +18 -0
  71. package/src/codex/ProviderModels.ts +185 -0
  72. package/src/codex/ProviderSwitch.ts +36 -0
  73. package/src/commands/CommandRouter.ts +7 -2
  74. package/src/commands/commandTypes.ts +1 -1
  75. package/src/console/ConsoleTurnPresenter.ts +3 -0
  76. package/src/feishu/CardRenderer.ts +84 -43
  77. package/src/feishu/FeishuConnector.ts +15 -5
  78. package/src/feishu/FeishuTurnPresenter.ts +46 -4
  79. package/src/feishu/InboundText.ts +11 -0
  80. package/src/feishu/MergedForwardMessage.ts +15 -5
  81. package/src/index.ts +3 -1
  82. package/src/local-files/LocalFileViewerServer.ts +179 -20
  83. package/src/local-files/MarkdownFileLinks.ts +64 -0
  84. package/src/local-files/TurnPreviewPage.ts +792 -0
  85. package/src/presentation/OutboundRouter.ts +8 -0
  86. package/src/presentation/TurnStateReducer.ts +175 -2
  87. package/src/presentation/turnViewTypes.ts +13 -0
  88. package/src/proxy/ProxySessionController.ts +547 -297
  89. package/src/runtime/types.ts +33 -3
  90. package/src/state/StateStore.ts +86 -90
  91. package/src/utils/formatStorageSize.ts +13 -0
  92. package/src/utils/semanticVersion.ts +28 -0
@@ -73,6 +73,17 @@ export type AgentEvent =
73
73
  turnId: string;
74
74
  lastTokens: number;
75
75
  cumulativeTokens: number;
76
+ contextTokens?: number;
77
+ }
78
+ | {
79
+ type: "context_compaction";
80
+ sessionId: string;
81
+ turnId: string;
82
+ phase: "started" | "completed";
83
+ compactionId?: string;
84
+ timestampMs?: number;
85
+ turnCount?: number;
86
+ storageBytes?: number;
76
87
  }
77
88
  | {
78
89
  type: "progress";
@@ -113,6 +124,11 @@ export interface RemoteCompletedTurnSummary {
113
124
  completedAt?: number;
114
125
  }
115
126
 
127
+ export interface RemoteTurnPage {
128
+ turns: RemoteCompletedTurnSummary[];
129
+ nextCursor?: string;
130
+ }
131
+
116
132
  export interface RemoteSessionSummary {
117
133
  id: string;
118
134
  title?: string;
@@ -151,6 +167,11 @@ export interface RemoteSessionActivity {
151
167
  activeTurnId?: string;
152
168
  }
153
169
 
170
+ export interface RemoteSessionMetrics {
171
+ turnCount: number;
172
+ storageBytes?: number;
173
+ }
174
+
154
175
  export interface RuntimeSession {
155
176
  localSessionId: string;
156
177
  remoteSessionId: string;
@@ -163,6 +184,8 @@ export interface RuntimeSession {
163
184
  reasoningEffort?: string;
164
185
  permissionMode: PermissionMode;
165
186
  activeTurnId?: string;
187
+ // Returned by reconciliation so status callers can reuse the same observation.
188
+ remoteSummary?: RemoteSessionSummary;
166
189
  }
167
190
 
168
191
  export interface CreateRuntimeSessionInput {
@@ -229,8 +252,10 @@ export interface AgentRuntime {
229
252
  getSession(localSessionId: string): RuntimeSession | undefined;
230
253
  readSessionMetadata(remoteSessionId: string): Promise<RuntimeSessionMetadata>;
231
254
  listRemoteSessions?(input?: { searchTerm?: string; cursor?: string; limit?: number }): Promise<RemoteSessionPage>;
232
- readRemoteSession?(remoteSessionId: string): Promise<RemoteSessionSummary>;
255
+ readRemoteSession?(remoteSessionId: string, view?: "metadata" | "latest" | "latest-full"): Promise<RemoteSessionSummary>;
256
+ readRemoteSessionMetrics?(remoteSessionId: string): Promise<RemoteSessionMetrics>;
233
257
  readRemoteForkSource?(remoteSessionId: string): Promise<RemoteSessionSummary>;
258
+ listRemoteTurnSummaries?(remoteSessionId: string, input: { cursor?: string; limit: number }): Promise<RemoteTurnPage>;
234
259
  inspectRemoteSessionActivity?(remoteSessionId: string): Promise<RemoteSessionActivity>;
235
260
  synchronizeSession(sessionId: string): Promise<RuntimeSession>;
236
261
  startTurn(sessionId: string, prompt: RuntimePrompt): Promise<string>;
@@ -246,9 +271,14 @@ export interface AgentRuntime {
246
271
  setModel(sessionId: string, model: string): Promise<void>;
247
272
  setReasoningEffort(sessionId: string, effort: string): Promise<void>;
248
273
  setPermissionMode(sessionId: string, mode: PermissionMode): Promise<void>;
249
- setExecutionSettings?(sessionId: string, settings: RuntimeExecutionSettings): Promise<RuntimeSession>;
274
+ setExecutionSettings?(
275
+ sessionId: string,
276
+ settings: RuntimeExecutionSettings,
277
+ // Called after remote verification, before releasing the task's operation lock.
278
+ persist?: (session: RuntimeSession) => Promise<void>,
279
+ ): Promise<RuntimeSession>;
250
280
  respondToApproval(sessionId: string, requestId: string, decision: ApprovalDecision): Promise<void>;
251
- listModels(): Promise<ModelOption[]>;
281
+ listModels(modelProvider?: string, fallbackModel?: string): Promise<ModelOption[]>;
252
282
  listModelProviders?(): Promise<ModelProviderOption[]>;
253
283
  release?(options?: { force?: boolean }): Promise<RuntimeReleaseResult>;
254
284
  onEvent(listener: (event: RuntimeEvent) => void): () => void;
@@ -95,6 +95,21 @@ export interface CompletedTurnSnapshotRecord {
95
95
  updatedAt: string;
96
96
  }
97
97
 
98
+ export interface CompletedTurnIndexRecord {
99
+ turnId: string;
100
+ parentTurnId?: string;
101
+ timestamp: number;
102
+ updatedAt: string;
103
+ }
104
+
105
+ export interface TurnPromptSummary {
106
+ status?: string;
107
+ prompt?: string;
108
+ taskTitle?: string;
109
+ startedAt?: number;
110
+ completedAt?: number;
111
+ }
112
+
98
113
  export interface ImportedCompletedTurnRecord {
99
114
  turnId: string;
100
115
  snapshot: unknown;
@@ -1032,6 +1047,21 @@ export class StateStore {
1032
1047
  return row ? JSON.parse(row.snapshot_json) : undefined;
1033
1048
  }
1034
1049
 
1050
+ getTurnPromptSummary(turnId: string): TurnPromptSummary | undefined {
1051
+ const row = this.db.prepare(`SELECT
1052
+ json_extract(snapshot_json, '$.status') AS status,
1053
+ json_extract(snapshot_json, '$.prompt') AS prompt,
1054
+ json_extract(snapshot_json, '$.taskTitle') AS taskTitle,
1055
+ json_extract(snapshot_json, '$.startedAt') AS startedAt,
1056
+ json_extract(snapshot_json, '$.completedAt') AS completedAt
1057
+ FROM turn_snapshots WHERE turn_id = ?`).get(turnId) as TurnPromptSummary | undefined;
1058
+ return row ? {
1059
+ status: row.status ?? undefined, prompt: row.prompt ?? undefined,
1060
+ taskTitle: row.taskTitle ?? undefined, startedAt: row.startedAt ?? undefined,
1061
+ completedAt: row.completedAt ?? undefined,
1062
+ } : undefined;
1063
+ }
1064
+
1035
1065
  findLatestTurnSnapshotForSession(localSessionId: string): PersistedTurnSnapshotRecord | undefined {
1036
1066
  const row = this.db.prepare(`
1037
1067
  SELECT turn_id, local_session_id, context_key, snapshot_json, updated_at
@@ -1096,16 +1126,16 @@ export class StateStore {
1096
1126
  : undefined;
1097
1127
  }
1098
1128
 
1099
- saveTurnParent(turnId: string, localSessionId: string, parentTurnId?: string): void {
1129
+ saveTurnParent(turnId: string, localSessionId: string, parentTurnId?: string, replaceExisting = false): void {
1100
1130
  this.db.prepare(`
1101
1131
  INSERT INTO turn_parent_links (
1102
1132
  turn_id, local_session_id, parent_turn_id, created_at
1103
1133
  ) VALUES (?, ?, ?, ?)
1104
1134
  ON CONFLICT(turn_id) DO UPDATE SET
1105
1135
  parent_turn_id = excluded.parent_turn_id
1106
- WHERE turn_parent_links.parent_turn_id IS NULL
1136
+ WHERE (turn_parent_links.parent_turn_id IS NULL OR ?)
1107
1137
  AND excluded.parent_turn_id IS NOT NULL
1108
- `).run(turnId, localSessionId, parentTurnId ?? null, new Date().toISOString());
1138
+ `).run(turnId, localSessionId, parentTurnId ?? null, new Date().toISOString(), Number(replaceExisting));
1109
1139
  }
1110
1140
 
1111
1141
  importCompletedTurnHistory(input: {
@@ -1114,6 +1144,7 @@ export class StateStore {
1114
1144
  agentName: string;
1115
1145
  remoteSessionId: string;
1116
1146
  turns: ImportedCompletedTurnRecord[];
1147
+ replaceParents?: boolean;
1117
1148
  }): void {
1118
1149
  const importHistory = this.db.transaction(() => {
1119
1150
  let parentTurnId: string | undefined;
@@ -1140,7 +1171,7 @@ export class StateStore {
1140
1171
  input.remoteSessionId,
1141
1172
  turn.updatedAt,
1142
1173
  );
1143
- this.saveTurnParent(turn.turnId, input.localSessionId, parentTurnId);
1174
+ this.saveTurnParent(turn.turnId, input.localSessionId, parentTurnId, input.replaceParents);
1144
1175
  parentTurnId = turn.turnId;
1145
1176
  }
1146
1177
  });
@@ -1156,13 +1187,13 @@ export class StateStore {
1156
1187
  return row?.parent_turn_id ?? undefined;
1157
1188
  }
1158
1189
 
1159
- findLatestCompletedTurnId(localSessionId: string, contextKey: string): string | undefined {
1190
+ findLatestCompletedTurnId(localSessionId: string, contextKey?: string): string | undefined {
1160
1191
  const row = this.db
1161
1192
  .prepare(`
1162
1193
  SELECT turn_id
1163
1194
  FROM turn_snapshots
1164
1195
  WHERE local_session_id = ?
1165
- AND context_key = ?
1196
+ AND (? IS NULL OR context_key = ?)
1166
1197
  AND json_extract(snapshot_json, '$.status') = 'completed'
1167
1198
  ORDER BY
1168
1199
  coalesce(
@@ -1173,7 +1204,7 @@ export class StateStore {
1173
1204
  updated_at DESC
1174
1205
  LIMIT 1
1175
1206
  `)
1176
- .get(localSessionId, contextKey) as { turn_id: string } | undefined;
1207
+ .get(localSessionId, contextKey ?? null, contextKey ?? null) as { turn_id: string } | undefined;
1177
1208
  return row?.turn_id;
1178
1209
  }
1179
1210
 
@@ -1296,23 +1327,60 @@ export class StateStore {
1296
1327
  }
1297
1328
 
1298
1329
  listTaskTurnGraph(localSessionId: string): CompletedTurnSnapshotRecord[] {
1299
- this.backfillTurnParents(localSessionId);
1300
- const records = new Map(
1301
- this.listCompletedTurnGraphRows(localSessionId).map((record) => [record.turnId, record]),
1302
- );
1330
+ return this.listTaskTurnGraphIndex(localSessionId).map((record) => ({
1331
+ ...record, snapshot: this.getTurnSnapshot(record.turnId),
1332
+ }));
1333
+ }
1334
+
1335
+ countTaskTurns(localSessionId: string): number {
1336
+ const turnIds = new Set(this.listTaskTurnGraphIndex(localSessionId).map((turn) => turn.turnId));
1337
+ const own = this.db.prepare(
1338
+ "SELECT turn_id AS turnId FROM turn_snapshots WHERE local_session_id = ?",
1339
+ ).all(localSessionId) as Array<{ turnId: string }>;
1340
+ for (const turn of own) turnIds.add(turn.turnId);
1341
+ return turnIds.size;
1342
+ }
1343
+
1344
+ listTaskTurnGraphIndex(localSessionId: string): CompletedTurnIndexRecord[] {
1345
+ const backfilled = new Set<string>();
1346
+ const backfill = (id: string): void => {
1347
+ if (backfilled.has(id)) return;
1348
+ this.backfillTurnParents(id);
1349
+ backfilled.add(id);
1350
+ };
1351
+ backfill(localSessionId);
1352
+ const projection = `SELECT ts.turn_id AS turnId, tpl.parent_turn_id AS parentTurnId,
1353
+ ts.local_session_id AS localSessionId, ts.updated_at AS updatedAt,
1354
+ coalesce(json_extract(ts.snapshot_json, '$.completedAt'),
1355
+ json_extract(ts.snapshot_json, '$.startedAt'),
1356
+ cast(strftime('%s', ts.updated_at) AS INTEGER) * 1000, 0) AS timestamp
1357
+ FROM turn_snapshots ts LEFT JOIN turn_parent_links tpl ON tpl.turn_id = ts.turn_id
1358
+ WHERE json_extract(ts.snapshot_json, '$.status') = 'completed'`;
1359
+ type Row = CompletedTurnIndexRecord & { localSessionId: string; parentTurnId: string | undefined };
1360
+ const normalize = (row: Row): CompletedTurnIndexRecord => ({
1361
+ turnId: row.turnId, parentTurnId: row.parentTurnId ?? undefined,
1362
+ timestamp: row.timestamp, updatedAt: row.updatedAt,
1363
+ });
1364
+ const own = this.db.prepare(`${projection} AND ts.local_session_id = ?`).all(localSessionId) as Row[];
1365
+ const read = this.db.prepare(`${projection} AND ts.turn_id = ?`);
1366
+ const records = new Map(own.map((row) => [row.turnId, normalize(row)]));
1303
1367
  let ancestorTurnId = this.findForkSourceTurnId(localSessionId)
1304
1368
  ?? this.findCrossSessionParentTurnId(localSessionId);
1305
1369
  const visited = new Set<string>();
1306
1370
 
1307
1371
  while (ancestorTurnId && !visited.has(ancestorTurnId)) {
1308
1372
  visited.add(ancestorTurnId);
1309
- const ancestor = this.getCompletedTurnGraphRecord(ancestorTurnId);
1373
+ let ancestor = read.get(ancestorTurnId) as Row | undefined;
1310
1374
  if (!ancestor) break;
1311
- records.set(ancestor.turnId, ancestor);
1375
+ if (!backfilled.has(ancestor.localSessionId)) {
1376
+ backfill(ancestor.localSessionId);
1377
+ ancestor = read.get(ancestorTurnId) as Row;
1378
+ }
1379
+ records.set(ancestor.turnId, normalize(ancestor));
1312
1380
  ancestorTurnId = ancestor.parentTurnId;
1313
1381
  }
1314
1382
 
1315
- return [...records.values()].sort(compareCompletedTurnSnapshots);
1383
+ return [...records.values()].sort((a, b) => b.timestamp - a.timestamp || b.updatedAt.localeCompare(a.updatedAt));
1316
1384
  }
1317
1385
 
1318
1386
  getForkHistorySource(localSessionId: string): ForkHistorySourceRecord | undefined {
@@ -1340,54 +1408,16 @@ export class StateStore {
1340
1408
  };
1341
1409
  }
1342
1410
 
1343
- hasImportedForkTurnHistory(localSessionId: string): boolean {
1411
+ hasImportedForkTurnHistory(localSessionId: string, sourceRemoteSessionId?: string, sourceTurnId?: string): boolean {
1344
1412
  return Boolean(this.db.prepare(`
1345
1413
  SELECT 1
1346
1414
  FROM audit_events
1347
1415
  WHERE event_type = 'fork_turn_history_imported'
1348
1416
  AND json_extract(payload_json, '$.forkedLocalSessionId') = ?
1417
+ AND (? IS NULL OR json_extract(payload_json, '$.sourceRemoteSessionId') = ?)
1418
+ AND (? IS NULL OR json_extract(payload_json, '$.sourceTurnId') = ?)
1349
1419
  LIMIT 1
1350
- `).get(localSessionId));
1351
- }
1352
-
1353
- private listCompletedTurnGraphRows(localSessionId: string): CompletedTurnSnapshotRecord[] {
1354
- const rows = this.db.prepare(`
1355
- SELECT ts.turn_id, tpl.parent_turn_id, ts.snapshot_json, ts.updated_at
1356
- FROM turn_snapshots ts
1357
- LEFT JOIN turn_parent_links tpl ON tpl.turn_id = ts.turn_id
1358
- WHERE ts.local_session_id = ?
1359
- AND json_extract(ts.snapshot_json, '$.status') = 'completed'
1360
- `).all(localSessionId) as Array<{
1361
- turn_id: string;
1362
- parent_turn_id: string | null;
1363
- snapshot_json: string;
1364
- updated_at: string;
1365
- }>;
1366
- return rows.map(mapCompletedTurnSnapshot);
1367
- }
1368
-
1369
- private getCompletedTurnGraphRecord(turnId: string): CompletedTurnSnapshotRecord | undefined {
1370
- const owner = this.db.prepare(`
1371
- SELECT local_session_id
1372
- FROM turn_snapshots
1373
- WHERE turn_id = ?
1374
- AND json_extract(snapshot_json, '$.status') = 'completed'
1375
- `).get(turnId) as { local_session_id: string } | undefined;
1376
- if (!owner) return undefined;
1377
- this.backfillTurnParents(owner.local_session_id);
1378
- const row = this.db.prepare(`
1379
- SELECT ts.turn_id, tpl.parent_turn_id, ts.snapshot_json, ts.updated_at
1380
- FROM turn_snapshots ts
1381
- LEFT JOIN turn_parent_links tpl ON tpl.turn_id = ts.turn_id
1382
- WHERE ts.turn_id = ?
1383
- AND json_extract(ts.snapshot_json, '$.status') = 'completed'
1384
- `).get(turnId) as {
1385
- turn_id: string;
1386
- parent_turn_id: string | null;
1387
- snapshot_json: string;
1388
- updated_at: string;
1389
- } | undefined;
1390
- return row ? mapCompletedTurnSnapshot(row) : undefined;
1420
+ `).get(localSessionId, sourceRemoteSessionId ?? null, sourceRemoteSessionId ?? null, sourceTurnId ?? null, sourceTurnId ?? null));
1391
1421
  }
1392
1422
 
1393
1423
  private findForkSourceTurnId(localSessionId: string): string | undefined {
@@ -2289,40 +2319,6 @@ function mapTurnAttempt(row: TurnAttemptRow): TurnAttemptRecord {
2289
2319
  };
2290
2320
  }
2291
2321
 
2292
- function mapCompletedTurnSnapshot(row: {
2293
- turn_id: string;
2294
- parent_turn_id: string | null;
2295
- snapshot_json: string;
2296
- updated_at: string;
2297
- }): CompletedTurnSnapshotRecord {
2298
- return {
2299
- turnId: row.turn_id,
2300
- parentTurnId: row.parent_turn_id ?? undefined,
2301
- snapshot: JSON.parse(row.snapshot_json),
2302
- updatedAt: row.updated_at,
2303
- };
2304
- }
2305
-
2306
- function compareCompletedTurnSnapshots(
2307
- left: CompletedTurnSnapshotRecord,
2308
- right: CompletedTurnSnapshotRecord,
2309
- ): number {
2310
- const timeDifference = completedTurnTimestamp(right) - completedTurnTimestamp(left);
2311
- return timeDifference || right.updatedAt.localeCompare(left.updatedAt);
2312
- }
2313
-
2314
- function completedTurnTimestamp(record: CompletedTurnSnapshotRecord): number {
2315
- const snapshot = record.snapshot && typeof record.snapshot === "object"
2316
- ? record.snapshot as { completedAt?: unknown; startedAt?: unknown }
2317
- : undefined;
2318
- const timestamp = typeof snapshot?.completedAt === "number"
2319
- ? snapshot.completedAt
2320
- : typeof snapshot?.startedAt === "number"
2321
- ? snapshot.startedAt
2322
- : Date.parse(record.updatedAt);
2323
- return Number.isFinite(timestamp) ? timestamp : 0;
2324
- }
2325
-
2326
2322
  function stringValue(value: unknown): string | undefined {
2327
2323
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
2328
2324
  }
@@ -0,0 +1,13 @@
1
+ export function formatStorageSize(bytes: number): string {
2
+ const normalized = Math.max(0, Math.round(bytes));
3
+ if (normalized < 1_024) return `${normalized} B`;
4
+ const units = ["KB", "MB", "GB", "TB"];
5
+ let value = normalized / 1_024;
6
+ let unitIndex = 0;
7
+ while (value >= 1_024 && unitIndex < units.length - 1) {
8
+ value /= 1_024;
9
+ unitIndex += 1;
10
+ }
11
+ const precision = value >= 100 ? 0 : value >= 10 ? 1 : 2;
12
+ return `${Number(value.toFixed(precision))} ${units[unitIndex]}`;
13
+ }
@@ -0,0 +1,28 @@
1
+ export function compareSemanticVersions(left: string, right: string): number {
2
+ const parse = (value: string) => /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value);
3
+ const a = parse(left);
4
+ const b = parse(right);
5
+ if (!a || !b) return 0;
6
+ for (let i = 1; i <= 3; i += 1) {
7
+ const difference = Number(a[i]) - Number(b[i]);
8
+ if (difference) return difference;
9
+ }
10
+ if (!a[4] && !b[4]) return 0;
11
+ if (!a[4]) return 1;
12
+ if (!b[4]) return -1;
13
+ const aParts = a[4].split(".");
14
+ const bParts = b[4].split(".");
15
+ for (let i = 0; i < Math.max(aParts.length, bParts.length); i += 1) {
16
+ const x = aParts[i];
17
+ const y = bParts[i];
18
+ if (x === undefined) return -1;
19
+ if (y === undefined) return 1;
20
+ if (x === y) continue;
21
+ const nx = /^\d+$/u.test(x);
22
+ const ny = /^\d+$/u.test(y);
23
+ if (nx && ny) return Number(x) - Number(y);
24
+ if (nx !== ny) return nx ? -1 : 1;
25
+ return x.localeCompare(y);
26
+ }
27
+ return 0;
28
+ }