sentinelayer-cli 0.4.5 → 0.8.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 (72) hide show
  1. package/README.md +16 -18
  2. package/package.json +7 -6
  3. package/src/agents/jules/config/definition.js +13 -62
  4. package/src/agents/jules/config/system-prompt.js +8 -1
  5. package/src/agents/jules/fix-cycle.js +12 -372
  6. package/src/agents/jules/loop.js +116 -26
  7. package/src/agents/jules/pulse.js +10 -327
  8. package/src/agents/jules/stream.js +13 -12
  9. package/src/agents/jules/swarm/orchestrator.js +3 -3
  10. package/src/agents/jules/swarm/sub-agent.js +6 -3
  11. package/src/agents/jules/tools/aidenid-email.js +189 -0
  12. package/src/agents/jules/tools/auth-audit.js +1187 -45
  13. package/src/agents/jules/tools/dispatch.js +25 -12
  14. package/src/agents/jules/tools/file-edit.js +2 -180
  15. package/src/agents/jules/tools/file-read.js +2 -100
  16. package/src/agents/jules/tools/glob.js +2 -168
  17. package/src/agents/jules/tools/grep.js +2 -228
  18. package/src/agents/jules/tools/path-guards.js +2 -161
  19. package/src/agents/jules/tools/runtime-audit.js +6 -2
  20. package/src/agents/jules/tools/shell.js +2 -383
  21. package/src/agents/persona-visuals.js +64 -0
  22. package/src/agents/shared-tools/dispatch-core.js +320 -0
  23. package/src/agents/shared-tools/file-edit.js +180 -0
  24. package/src/agents/shared-tools/file-read.js +100 -0
  25. package/src/agents/shared-tools/glob.js +168 -0
  26. package/src/agents/shared-tools/grep.js +228 -0
  27. package/src/agents/shared-tools/index.js +46 -0
  28. package/src/agents/shared-tools/path-guards.js +161 -0
  29. package/src/agents/shared-tools/shell.js +383 -0
  30. package/src/ai/aidenid.js +56 -7
  31. package/src/ai/client.js +45 -0
  32. package/src/ai/proxy.js +137 -0
  33. package/src/auth/gate.js +290 -16
  34. package/src/auth/http.js +450 -39
  35. package/src/auth/service.js +262 -47
  36. package/src/auth/session-store.js +475 -21
  37. package/src/cli.js +5 -0
  38. package/src/commands/audit.js +13 -8
  39. package/src/commands/auth.js +53 -9
  40. package/src/commands/omargate.js +10 -2
  41. package/src/commands/scan.js +10 -4
  42. package/src/commands/session.js +590 -0
  43. package/src/commands/spec.js +62 -0
  44. package/src/commands/watch.js +3 -2
  45. package/src/daemon/assignment-ledger.js +196 -0
  46. package/src/daemon/error-worker.js +599 -16
  47. package/src/daemon/fix-cycle.js +384 -0
  48. package/src/daemon/ingest-refresh.js +10 -9
  49. package/src/daemon/jira-lifecycle.js +135 -0
  50. package/src/daemon/pulse.js +327 -0
  51. package/src/daemon/scope-engine.js +1068 -0
  52. package/src/events/schema.js +190 -0
  53. package/src/interactive/index.js +18 -16
  54. package/src/legacy-cli.js +606 -37
  55. package/src/prompt/generator.js +19 -1
  56. package/src/review/ai-review.js +11 -1
  57. package/src/review/local-review.js +75 -19
  58. package/src/review/omargate-interactive.js +68 -0
  59. package/src/review/omargate-orchestrator.js +404 -0
  60. package/src/review/persona-prompts.js +296 -0
  61. package/src/review/scan-modes.js +48 -0
  62. package/src/scan/generator.js +1 -1
  63. package/src/session/agent-registry.js +352 -0
  64. package/src/session/daemon.js +801 -0
  65. package/src/session/paths.js +33 -0
  66. package/src/session/runtime-bridge.js +739 -0
  67. package/src/session/store.js +388 -0
  68. package/src/session/stream.js +325 -0
  69. package/src/spec/generator.js +100 -0
  70. package/src/telemetry/session-tracker.js +148 -32
  71. package/src/telemetry/sync.js +6 -2
  72. package/src/ui/command-hints.js +13 -0
@@ -6,11 +6,89 @@ import process from "node:process";
6
6
 
7
7
  const CREDENTIALS_VERSION = 1;
8
8
  const KEYRING_SERVICE = "sentinelayer-cli";
9
+ const SESSION_WARNING_PREFIX = "sentinelayer.auth.session";
10
+ const SESSION_WARNING_REDACT_KEYS = /token|secret|password|key|authorization/i;
11
+ const SESSION_WARNING_MAX_VALUE_LENGTH = 200;
12
+ const SESSION_WARNING_ALLOWED_FIELDS = new Set([
13
+ "reason",
14
+ "source",
15
+ "operation",
16
+ "storage",
17
+ "codeHint",
18
+ "requestIdHash",
19
+ ]);
9
20
 
10
21
  function nowIso() {
11
22
  return new Date().toISOString();
12
23
  }
13
24
 
25
+ function createSessionWarningId() {
26
+ try {
27
+ return crypto.randomUUID();
28
+ } catch {
29
+ return `session-${Date.now().toString(36)}-${crypto.randomBytes(8).toString("hex")}`;
30
+ }
31
+ }
32
+
33
+ function sanitizeSessionWarningValue(value, depth = 0) {
34
+ if (value === null || value === undefined) {
35
+ return value;
36
+ }
37
+ if (depth > 3) {
38
+ return "[TRUNCATED]";
39
+ }
40
+ if (Array.isArray(value)) {
41
+ return value.map((entry) => sanitizeSessionWarningValue(entry, depth + 1));
42
+ }
43
+ if (typeof value === "object") {
44
+ const sanitized = {};
45
+ for (const [key, entry] of Object.entries(value)) {
46
+ if (SESSION_WARNING_REDACT_KEYS.test(key)) {
47
+ sanitized[key] = "[REDACTED]";
48
+ continue;
49
+ }
50
+ sanitized[key] = sanitizeSessionWarningValue(entry, depth + 1);
51
+ }
52
+ return sanitized;
53
+ }
54
+ if (typeof value === "string") {
55
+ if (value.length > SESSION_WARNING_MAX_VALUE_LENGTH) {
56
+ return `${value.slice(0, SESSION_WARNING_MAX_VALUE_LENGTH)}…`;
57
+ }
58
+ return value;
59
+ }
60
+ return value;
61
+ }
62
+
63
+ function sanitizeSessionWarningDetails(details) {
64
+ if (!details || typeof details !== "object") {
65
+ return {};
66
+ }
67
+ return sanitizeSessionWarningValue(details, 0);
68
+ }
69
+
70
+ function emitSessionWarning(code, details = {}) {
71
+ const sanitizedDetails = sanitizeSessionWarningDetails(details);
72
+ const payload = {
73
+ level: "warn",
74
+ code: String(code || "SESSION_WARNING").toUpperCase(),
75
+ warningId: createSessionWarningId(),
76
+ timestamp: nowIso(),
77
+ };
78
+ for (const [key, value] of Object.entries(sanitizedDetails)) {
79
+ if (SESSION_WARNING_ALLOWED_FIELDS.has(key)) {
80
+ payload[key] = value;
81
+ } else {
82
+ payload[key] = "[OMITTED]";
83
+ }
84
+ }
85
+ try {
86
+ console.warn(`${SESSION_WARNING_PREFIX} ${JSON.stringify(payload)}`);
87
+ } catch {
88
+ console.warn(`${SESSION_WARNING_PREFIX} ${payload.code}`);
89
+ }
90
+ }
91
+
14
92
  function resolveHomeDir(homeDir) {
15
93
  return path.resolve(String(homeDir || os.homedir()));
16
94
  }
@@ -26,6 +104,96 @@ export function resolveCredentialsFilePath({ homeDir } = {}) {
26
104
  return path.join(resolvedHome, ".sentinelayer", "credentials.json");
27
105
  }
28
106
 
107
+ function resolveCredentialsKeyPath({ homeDir } = {}) {
108
+ const resolvedHome = resolveHomeDir(homeDir);
109
+ return path.join(resolvedHome, ".sentinelayer", "keys", "credentials.key");
110
+ }
111
+
112
+ function resolveLegacyCredentialsKeyPath({ homeDir } = {}) {
113
+ const resolvedHome = resolveHomeDir(homeDir);
114
+ return path.join(resolvedHome, ".sentinelayer", "credentials.key");
115
+ }
116
+
117
+ async function loadOrCreateFileKey({ homeDir } = {}) {
118
+ const keyPath = resolveCredentialsKeyPath({ homeDir });
119
+ const legacyKeyPath = resolveLegacyCredentialsKeyPath({ homeDir });
120
+ try {
121
+ const raw = await fsp.readFile(keyPath, "utf-8");
122
+ const key = Buffer.from(String(raw || "").trim(), "base64");
123
+ if (key.length === 32) {
124
+ return key;
125
+ }
126
+ } catch (error) {
127
+ if (!(error && typeof error === "object" && error.code === "ENOENT")) {
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ try {
133
+ const legacyRaw = await fsp.readFile(legacyKeyPath, "utf-8");
134
+ const legacyKey = Buffer.from(String(legacyRaw || "").trim(), "base64");
135
+ if (legacyKey.length === 32) {
136
+ await fsp.mkdir(path.dirname(keyPath), { recursive: true, mode: 0o700 });
137
+ await fsp.writeFile(keyPath, legacyKey.toString("base64"), { encoding: "utf-8", mode: 0o600 });
138
+ try {
139
+ await fsp.chmod(keyPath, 0o600);
140
+ } catch {
141
+ // Windows does not reliably support POSIX chmod semantics.
142
+ }
143
+ let verified = false;
144
+ try {
145
+ const verifyRaw = await fsp.readFile(keyPath, "utf-8");
146
+ const verifyKey = Buffer.from(String(verifyRaw || "").trim(), "base64");
147
+ verified = verifyKey.length === 32 && verifyKey.equals(legacyKey);
148
+ } catch {
149
+ verified = false;
150
+ }
151
+ if (verified) {
152
+ await fsp.rm(legacyKeyPath, { force: true });
153
+ }
154
+ return legacyKey;
155
+ }
156
+ } catch (error) {
157
+ if (!(error && typeof error === "object" && error.code === "ENOENT")) {
158
+ throw error;
159
+ }
160
+ }
161
+
162
+ const key = crypto.randomBytes(32);
163
+ await fsp.mkdir(path.dirname(keyPath), { recursive: true, mode: 0o700 });
164
+ await fsp.writeFile(keyPath, key.toString("base64"), { encoding: "utf-8", mode: 0o600 });
165
+ try {
166
+ await fsp.chmod(keyPath, 0o600);
167
+ } catch {
168
+ // Windows does not reliably support POSIX chmod semantics.
169
+ }
170
+ return key;
171
+ }
172
+
173
+ function encryptToken(token, key) {
174
+ const iv = crypto.randomBytes(12);
175
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
176
+ const ciphertext = Buffer.concat([cipher.update(token, "utf-8"), cipher.final()]);
177
+ const tag = cipher.getAuthTag();
178
+ return {
179
+ tokenEncrypted: ciphertext.toString("base64"),
180
+ tokenIv: iv.toString("base64"),
181
+ tokenTag: tag.toString("base64"),
182
+ };
183
+ }
184
+
185
+ function decryptToken({ tokenEncrypted, tokenIv, tokenTag }, key) {
186
+ const iv = Buffer.from(tokenIv, "base64");
187
+ const tag = Buffer.from(tokenTag, "base64");
188
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
189
+ decipher.setAuthTag(tag);
190
+ const plaintext = Buffer.concat([
191
+ decipher.update(Buffer.from(tokenEncrypted, "base64")),
192
+ decipher.final(),
193
+ ]);
194
+ return plaintext.toString("utf-8");
195
+ }
196
+
29
197
  function buildKeyringAccountName(apiUrl) {
30
198
  const digest = crypto
31
199
  .createHash("sha256")
@@ -72,10 +240,122 @@ function normalizeMetadata(raw = {}) {
72
240
  updatedAt: String(raw.updatedAt || "").trim() || nowIso(),
73
241
  user: normalizeUser(raw.user),
74
242
  token: String(raw.token || "").trim() || null,
243
+ tokenEncrypted: String(raw.tokenEncrypted || "").trim() || null,
244
+ tokenIv: String(raw.tokenIv || "").trim() || null,
245
+ tokenTag: String(raw.tokenTag || "").trim() || null,
75
246
  aidenid: normalizeAidenId(raw.aidenid),
76
247
  };
77
248
  }
78
249
 
250
+ function isRetriableRenameError(error) {
251
+ if (!error || typeof error !== "object") {
252
+ return false;
253
+ }
254
+ return (
255
+ error.code === "EPERM" ||
256
+ error.code === "EACCES" ||
257
+ error.code === "EEXIST" ||
258
+ error.code === "EBUSY"
259
+ );
260
+ }
261
+
262
+ function delayMilliseconds(ms) {
263
+ return new Promise((resolve) => setTimeout(resolve, ms));
264
+ }
265
+
266
+ async function renameWithRetry(sourcePath, destinationPath, { attempts = 3, baseDelayMs = 25 } = {}) {
267
+ let lastError = null;
268
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
269
+ try {
270
+ await fsp.rename(sourcePath, destinationPath);
271
+ return;
272
+ } catch (error) {
273
+ lastError = error;
274
+ if (!isRetriableRenameError(error) || attempt >= attempts) {
275
+ break;
276
+ }
277
+ await delayMilliseconds(baseDelayMs * attempt);
278
+ }
279
+ }
280
+ throw lastError;
281
+ }
282
+
283
+ async function syncFile(filePath) {
284
+ const normalizedPath = String(filePath || "").trim();
285
+ if (!normalizedPath) {
286
+ return;
287
+ }
288
+ let handle = null;
289
+ try {
290
+ handle = await fsp.open(normalizedPath, "r");
291
+ await handle.sync();
292
+ } catch {
293
+ // Best effort only. Some filesystems/runtimes do not support explicit fsync.
294
+ } finally {
295
+ if (handle) {
296
+ await handle.close().catch(() => {});
297
+ }
298
+ }
299
+ }
300
+
301
+ async function syncDirectory(directoryPath) {
302
+ const normalizedPath = String(directoryPath || "").trim();
303
+ if (!normalizedPath) {
304
+ return;
305
+ }
306
+ let handle = null;
307
+ try {
308
+ handle = await fsp.open(normalizedPath, "r");
309
+ await handle.sync();
310
+ } catch {
311
+ // Directory fsync support is platform-dependent; ignore when unsupported.
312
+ } finally {
313
+ if (handle) {
314
+ await handle.close().catch(() => {});
315
+ }
316
+ }
317
+ }
318
+
319
+ async function replaceWithBackup(tmpPath, filePath) {
320
+ const directory = path.dirname(filePath);
321
+ const backupPath = path.join(
322
+ directory,
323
+ `.credentials.backup.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString("hex")}.bak`
324
+ );
325
+ let backupCreated = false;
326
+ try {
327
+ try {
328
+ await fsp.rename(filePath, backupPath);
329
+ backupCreated = true;
330
+ await syncDirectory(directory);
331
+ } catch (error) {
332
+ if (!(error && typeof error === "object" && error.code === "ENOENT")) {
333
+ throw error;
334
+ }
335
+ }
336
+
337
+ await renameWithRetry(tmpPath, filePath, { attempts: 3, baseDelayMs: 40 });
338
+ await syncFile(filePath);
339
+ await syncDirectory(directory);
340
+
341
+ if (backupCreated) {
342
+ await fsp.rm(backupPath, { force: true }).catch(() => {});
343
+ await syncDirectory(directory);
344
+ }
345
+ } catch (error) {
346
+ if (backupCreated) {
347
+ try {
348
+ await fsp.rename(backupPath, filePath);
349
+ await syncFile(filePath);
350
+ await syncDirectory(directory);
351
+ } catch {
352
+ // Best-effort restore; keep original error as the primary failure.
353
+ }
354
+ }
355
+ throw error;
356
+ }
357
+ }
358
+
79
359
  async function loadKeytarClient() {
80
360
  const disableKeyring = String(process.env.SENTINELAYER_DISABLE_KEYRING || "")
81
361
  .trim()
@@ -83,6 +363,18 @@ async function loadKeytarClient() {
83
363
  if (disableKeyring === "1" || disableKeyring === "true" || disableKeyring === "yes" || disableKeyring === "on") {
84
364
  return null;
85
365
  }
366
+ const keyringMode = String(process.env.SENTINELAYER_KEYRING_MODE || "")
367
+ .trim()
368
+ .toLowerCase();
369
+ const enableKeyring =
370
+ keyringMode === "keyring" ||
371
+ keyringMode === "enabled" ||
372
+ keyringMode === "on" ||
373
+ keyringMode === "true" ||
374
+ keyringMode === "1";
375
+ if (!enableKeyring) {
376
+ return null;
377
+ }
86
378
  try {
87
379
  const mod = await import("keytar");
88
380
  const client = mod && typeof mod === "object" ? mod.default || mod : null;
@@ -120,16 +412,117 @@ async function readMetadata({ homeDir } = {}) {
120
412
  }
121
413
 
122
414
  async function writeMetadata(filePath, metadata) {
123
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
124
- await fsp.writeFile(filePath, `${JSON.stringify(metadata, null, 2)}\n`, {
125
- encoding: "utf-8",
126
- mode: 0o600,
127
- });
415
+ const directory = path.dirname(filePath);
416
+ await fsp.mkdir(directory, { recursive: true, mode: 0o700 });
128
417
  try {
129
- await fsp.chmod(filePath, 0o600);
418
+ await fsp.chmod(directory, 0o700);
130
419
  } catch {
131
420
  // Windows does not reliably support POSIX chmod semantics.
132
421
  }
422
+ const tmpPath = path.join(
423
+ directory,
424
+ `.credentials.${process.pid}.${Date.now()}.${crypto.randomBytes(6).toString("hex")}.tmp`
425
+ );
426
+ const payload = `${JSON.stringify(metadata, null, 2)}\n`;
427
+ try {
428
+ let tmpHandle = null;
429
+ try {
430
+ tmpHandle = await fsp.open(tmpPath, "w", 0o600);
431
+ await tmpHandle.writeFile(payload, { encoding: "utf-8" });
432
+ await tmpHandle.sync();
433
+ } finally {
434
+ if (tmpHandle) {
435
+ await tmpHandle.close().catch(() => {});
436
+ }
437
+ }
438
+ try {
439
+ await fsp.chmod(tmpPath, 0o600);
440
+ } catch {
441
+ // Windows does not reliably support POSIX chmod semantics.
442
+ }
443
+ try {
444
+ await renameWithRetry(tmpPath, filePath, { attempts: 3, baseDelayMs: 25 });
445
+ } catch (error) {
446
+ if (isRetriableRenameError(error)) {
447
+ await replaceWithBackup(tmpPath, filePath);
448
+ } else {
449
+ throw error;
450
+ }
451
+ }
452
+ await syncFile(filePath);
453
+ await syncDirectory(directory);
454
+ } finally {
455
+ await fsp.rm(tmpPath, { force: true }).catch(() => {});
456
+ }
457
+ }
458
+
459
+ async function migratePlaintextTokenIfNeeded({ metadata, filePath, homeDir } = {}) {
460
+ if (!metadata || !metadata.token) {
461
+ return { metadata, token: null, migrated: false };
462
+ }
463
+
464
+ const plaintextToken = String(metadata.token || "").trim();
465
+ if (!plaintextToken) {
466
+ return { metadata: { ...metadata, token: null }, token: null, migrated: false };
467
+ }
468
+
469
+ const updatedAt = nowIso();
470
+ const nextMetadata = normalizeMetadata({
471
+ ...metadata,
472
+ token: null,
473
+ updatedAt,
474
+ });
475
+
476
+ const keytar = await loadKeytarClient();
477
+ if (keytar) {
478
+ const keyringAccount = metadata.keyringAccount || buildKeyringAccountName(metadata.apiUrl);
479
+ await keytar.setPassword(KEYRING_SERVICE, keyringAccount, plaintextToken);
480
+ nextMetadata.storage = "keyring";
481
+ nextMetadata.keyringService = KEYRING_SERVICE;
482
+ nextMetadata.keyringAccount = keyringAccount;
483
+ const key = await loadOrCreateFileKey({ homeDir });
484
+ const encrypted = encryptToken(plaintextToken, key);
485
+ nextMetadata.tokenEncrypted = encrypted.tokenEncrypted;
486
+ nextMetadata.tokenIv = encrypted.tokenIv;
487
+ nextMetadata.tokenTag = encrypted.tokenTag;
488
+ } else {
489
+ const key = await loadOrCreateFileKey({ homeDir });
490
+ const encrypted = encryptToken(plaintextToken, key);
491
+ nextMetadata.storage = "file";
492
+ nextMetadata.keyringService = KEYRING_SERVICE;
493
+ nextMetadata.keyringAccount = "";
494
+ nextMetadata.tokenEncrypted = encrypted.tokenEncrypted;
495
+ nextMetadata.tokenIv = encrypted.tokenIv;
496
+ nextMetadata.tokenTag = encrypted.tokenTag;
497
+ }
498
+
499
+ await writeMetadata(filePath, nextMetadata);
500
+ const { metadata: verify } = await readMetadata({ homeDir });
501
+ if (verify && verify.token) {
502
+ throw new Error("Plaintext token migration failed: token field persisted.");
503
+ }
504
+
505
+ return { metadata: nextMetadata, token: plaintextToken, migrated: true };
506
+ }
507
+
508
+ async function tryDecryptFileToken({ metadata, homeDir }) {
509
+ if (!metadata || !metadata.tokenEncrypted || !metadata.tokenIv || !metadata.tokenTag) {
510
+ return null;
511
+ }
512
+ try {
513
+ const key = await loadOrCreateFileKey({ homeDir });
514
+ const token = decryptToken(
515
+ {
516
+ tokenEncrypted: metadata.tokenEncrypted,
517
+ tokenIv: metadata.tokenIv,
518
+ tokenTag: metadata.tokenTag,
519
+ },
520
+ key
521
+ );
522
+ return token || null;
523
+ } catch {
524
+ return null;
525
+ }
133
526
  }
134
527
 
135
528
  /**
@@ -164,27 +557,75 @@ export async function readStoredSession({ homeDir } = {}) {
164
557
  return null;
165
558
  }
166
559
 
560
+ if (metadata.token) {
561
+ const migrated = await migratePlaintextTokenIfNeeded({ metadata, filePath, homeDir });
562
+ if (migrated.migrated) {
563
+ return {
564
+ ...migrated.metadata,
565
+ filePath,
566
+ token: migrated.token,
567
+ storage: migrated.metadata.storage || "file",
568
+ };
569
+ }
570
+ }
571
+
167
572
  if (metadata.storage === "keyring") {
168
573
  const keytar = await loadKeytarClient();
169
- if (!keytar || !metadata.keyringAccount) {
170
- return null;
574
+ let keyringError = null;
575
+ if (keytar && metadata.keyringAccount) {
576
+ try {
577
+ const token = await keytar.getPassword(
578
+ metadata.keyringService || KEYRING_SERVICE,
579
+ metadata.keyringAccount
580
+ );
581
+ if (token) {
582
+ return {
583
+ ...metadata,
584
+ filePath,
585
+ token,
586
+ storage: "keyring",
587
+ };
588
+ }
589
+ keyringError = new Error("Keyring token not found");
590
+ } catch (error) {
591
+ keyringError = error instanceof Error ? error : new Error("Keyring access failed");
592
+ }
593
+ } else {
594
+ keyringError = new Error("Keyring unavailable");
171
595
  }
172
- const token = await keytar.getPassword(
173
- metadata.keyringService || KEYRING_SERVICE,
174
- metadata.keyringAccount
175
- );
176
- if (!token) {
177
- return null;
596
+ const fallbackToken = await tryDecryptFileToken({ metadata, homeDir });
597
+ if (fallbackToken) {
598
+ emitSessionWarning("KEYRING_FALLBACK_USED", {
599
+ reason: keyringError ? String(keyringError.message || keyringError) : "unknown",
600
+ source: "file-encrypted",
601
+ });
602
+ return {
603
+ ...metadata,
604
+ filePath,
605
+ token: fallbackToken,
606
+ storage: "file",
607
+ };
178
608
  }
179
- return {
180
- ...metadata,
181
- filePath,
182
- token,
183
- storage: "keyring",
184
- };
609
+ emitSessionWarning("KEYRING_FALLBACK_UNAVAILABLE", {
610
+ reason: keyringError ? String(keyringError.message || keyringError) : "unknown",
611
+ source: "keyring",
612
+ });
613
+ return null;
185
614
  }
186
615
 
187
616
  if (!metadata.token) {
617
+ if (metadata.tokenEncrypted && metadata.tokenIv && metadata.tokenTag) {
618
+ const token = await tryDecryptFileToken({ metadata, homeDir });
619
+ if (!token) {
620
+ return null;
621
+ }
622
+ return {
623
+ ...metadata,
624
+ filePath,
625
+ token,
626
+ storage: "file",
627
+ };
628
+ }
188
629
  return null;
189
630
  }
190
631
  return {
@@ -226,6 +667,14 @@ export async function readStoredSessionMetadata({ homeDir } = {}) {
226
667
  if (!metadata) {
227
668
  return null;
228
669
  }
670
+ if (metadata.token) {
671
+ const migrated = await migratePlaintextTokenIfNeeded({ metadata, filePath, homeDir });
672
+ return {
673
+ ...migrated.metadata,
674
+ filePath,
675
+ token: null,
676
+ };
677
+ }
229
678
  return {
230
679
  ...metadata,
231
680
  filePath,
@@ -319,7 +768,12 @@ export async function writeStoredSession(
319
768
  nextMetadata.storage = "file";
320
769
  nextMetadata.keyringService = KEYRING_SERVICE;
321
770
  nextMetadata.keyringAccount = "";
322
- nextMetadata.token = normalizedToken;
771
+ const key = await loadOrCreateFileKey({ homeDir });
772
+ const encrypted = encryptToken(normalizedToken, key);
773
+ nextMetadata.token = null;
774
+ nextMetadata.tokenEncrypted = encrypted.tokenEncrypted;
775
+ nextMetadata.tokenIv = encrypted.tokenIv;
776
+ nextMetadata.tokenTag = encrypted.tokenTag;
323
777
  }
324
778
 
325
779
  await writeMetadata(filePath, nextMetadata);
package/src/cli.js CHANGED
@@ -119,6 +119,11 @@ const COMMAND_REGISTRARS = {
119
119
  exportName: "registerDaemonCommand",
120
120
  needsLegacy: false,
121
121
  },
122
+ session: {
123
+ loader: () => import("./commands/session.js"),
124
+ exportName: "registerSessionCommand",
125
+ needsLegacy: false,
126
+ },
122
127
  };
123
128
 
124
129
  const COMMAND_SET = new Set(Object.keys(COMMAND_REGISTRARS));
@@ -8,6 +8,7 @@ import { loadAuditRunReport, resolveAuditRunDirectory, writeDdPackage } from "..
8
8
  import { writeAuditComparisonArtifact } from "../audit/replay.js";
9
9
  import { loadAuditRegistry, selectAuditAgents } from "../audit/registry.js";
10
10
  import { resolveOutputRoot } from "../config/service.js";
11
+ import { createAgentEvent } from "../events/schema.js";
11
12
  import { buildLegacyArgs } from "./legacy-args.js";
12
13
 
13
14
  function shouldEmitJson(options, command) {
@@ -939,12 +940,16 @@ export function registerAuditCommand(program, invokeLegacy) {
939
940
  const reconciliation = reconcileWithBaseline(julesFindings, omarBaseline);
940
941
 
941
942
  if (onEvent && reconciliation.summary) {
942
- onEvent({
943
- stream: "sl_event", event: "reconciliation_complete",
944
- agent: { id: JULES_DEFINITION.id, persona: JULES_DEFINITION.persona,
945
- color: JULES_DEFINITION.color, avatar: JULES_DEFINITION.avatar },
943
+ onEvent(createAgentEvent({
944
+ event: "reconciliation_complete",
945
+ agent: {
946
+ id: JULES_DEFINITION.id,
947
+ persona: JULES_DEFINITION.persona,
948
+ color: JULES_DEFINITION.color,
949
+ avatar: JULES_DEFINITION.avatar,
950
+ },
946
951
  payload: reconciliation.summary,
947
- });
952
+ }));
948
953
  }
949
954
 
950
955
  // ── [9] FINAL REPORT ──────────────────────────────────────────
@@ -1157,10 +1162,10 @@ function buildEventHandler(emitStream, emitJson, def) {
1157
1162
 
1158
1163
  function emitProgress(onEvent, def, message) {
1159
1164
  if (onEvent) {
1160
- onEvent({
1161
- stream: "sl_event", event: "progress",
1165
+ onEvent(createAgentEvent({
1166
+ event: "progress",
1162
1167
  agent: { id: def.id, persona: def.persona, color: def.color, avatar: def.avatar },
1163
1168
  payload: { phase: "setup", message },
1164
- });
1169
+ }));
1165
1170
  }
1166
1171
  }