@sift-wiki/cli 0.1.3 → 0.1.5

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 (3) hide show
  1. package/README.md +52 -22
  2. package/dist/bin/sift.js +2069 -542
  3. package/package.json +1 -1
package/dist/bin/sift.js CHANGED
@@ -137,6 +137,8 @@ function parseSearchQuery(input) {
137
137
  function parseContextQuery(input) {
138
138
  return {
139
139
  query: requireString(input, "query"),
140
+ queryIssuedAt: optionalString(input, "queryIssuedAt"),
141
+ timezone: optionalString(input, "timezone"),
140
142
  maxChars: requireInteger(input, "maxChars", 4e3)
141
143
  };
142
144
  }
@@ -147,20 +149,20 @@ function parseProfileQuery(input) {
147
149
  };
148
150
  }
149
151
  function parseToolsSearch(input) {
150
- const result2 = {
152
+ const result = {
151
153
  intent: requireString(input, "intent")
152
154
  };
153
155
  if (input.toolsetNames !== void 0) {
154
- result2.toolsetNames = requireStringArray(input, "toolsetNames");
156
+ result.toolsetNames = requireStringArray(input, "toolsetNames");
155
157
  }
156
158
  if (input.limit !== void 0) {
157
159
  const limit = optionalInteger(input, "limit");
158
160
  if (limit === void 0 || limit < 1 || limit > 20) {
159
161
  throw new Error("limit must be an integer between 1 and 20.");
160
162
  }
161
- result2.limit = limit;
163
+ result.limit = limit;
162
164
  }
163
- return result2;
165
+ return result;
164
166
  }
165
167
  function parseDecision(input) {
166
168
  return {
@@ -371,10 +373,24 @@ function writeTool(name, summary, properties, cliExample, options) {
371
373
  capability: "record:write",
372
374
  mutability: "write",
373
375
  transports: writeTransports,
376
+ idempotency: options?.idempotency,
374
377
  cliExample,
375
378
  hostedAgent: options?.hostedAgent
376
379
  });
377
380
  }
381
+ function hostedAgentOnlyReadTool(name, summary, properties, options) {
382
+ return defineTool({
383
+ name,
384
+ summary,
385
+ properties,
386
+ required: options.required,
387
+ capability: "record:read",
388
+ mutability: "read",
389
+ transports: [],
390
+ cliExample: "",
391
+ hostedAgent: { available: true, ...options.hostedAgent }
392
+ });
393
+ }
378
394
  function sourceWriteTool(name, summary, properties, cliExample) {
379
395
  return defineTool({
380
396
  name,
@@ -413,7 +429,7 @@ function defineTool(input) {
413
429
  mutability: input.mutability,
414
430
  auditCategory: input.mutability === "admin" ? "admin" : input.mutability,
415
431
  transports: input.transports,
416
- idempotency: input.mutability === "write" ? "recommended" : "none",
432
+ idempotency: input.idempotency ?? (input.mutability === "write" ? "recommended" : "none"),
417
433
  resultSize: "compact",
418
434
  cliExample: input.cliExample,
419
435
  hostedAgent: hostedAgentMetadata(input, required)
@@ -438,31 +454,8 @@ function defaultRiskClass(mutability) {
438
454
  return "low";
439
455
  }
440
456
  function defaultToolsets(name) {
441
- const [prefix] = name.split(".");
442
- switch (prefix) {
443
- case "decision":
444
- case "task":
445
- return ["work"];
446
- case "skill":
447
- return ["brain", "work"];
448
- case "record":
449
- case "source":
450
- case "capture":
451
- case "ingestion":
452
- return ["brain", "ingestion"];
453
- case "search":
454
- case "context":
455
- case "evidence":
456
- case "graph":
457
- return ["brain", "retrieval"];
458
- case "tools":
459
- return ["registry"];
460
- case "audit":
461
- case "event":
462
- return ["audit"];
463
- default:
464
- return ["brain"];
465
- }
457
+ const [prefix = ""] = name.split(".");
458
+ return defaultToolsetsByPrefix[prefix] ?? ["brain"];
466
459
  }
467
460
  function defaultSearchTerms(name, summary) {
468
461
  return [.../* @__PURE__ */ new Set([...tokenize(name), ...tokenize(summary)])];
@@ -473,7 +466,7 @@ function tokenize(text) {
473
466
  function stringProps(names) {
474
467
  return Object.fromEntries(names.map((name) => [name, { type: "string" }]));
475
468
  }
476
- var readTransports, writeTransports, NO_CAPABILITY, toolDefinitions;
469
+ var readTransports, writeTransports, NO_CAPABILITY, defaultToolsetsByPrefix, toolDefinitions;
477
470
  var init_registry = __esm({
478
471
  "../tools/dist/registry.js"() {
479
472
  "use strict";
@@ -483,6 +476,23 @@ var init_registry = __esm({
483
476
  readTransports = ["cli", "hosted_mcp", "local_mcp"];
484
477
  writeTransports = ["cli", "hosted_mcp", "local_mcp"];
485
478
  NO_CAPABILITY = "none";
479
+ defaultToolsetsByPrefix = {
480
+ audit: ["audit"],
481
+ capture: ["brain", "ingestion"],
482
+ context: ["brain", "retrieval"],
483
+ decision: ["work"],
484
+ event: ["audit"],
485
+ evidence: ["brain", "retrieval"],
486
+ graph: ["brain", "retrieval"],
487
+ ingestion: ["brain", "ingestion"],
488
+ record: ["brain", "ingestion"],
489
+ search: ["brain", "retrieval"],
490
+ skill: ["brain", "work"],
491
+ source: ["brain", "ingestion"],
492
+ task: ["work"],
493
+ tools: ["registry"],
494
+ web: ["web"]
495
+ };
486
496
  toolDefinitions = [
487
497
  readTool("contract.get", "Fetch the Sift agent contract (kernel + workspace overlay) and the contractVersion to echo on every gated tool call. Call this before any other Sift work.", {}, "sift contract get"),
488
498
  readTool("whoami", "Return principal, actor, scope, and capabilities.", {}, "sift whoami"),
@@ -601,16 +611,94 @@ var init_registry = __esm({
601
611
  severity: { type: "string" },
602
612
  visibility: { type: "array", items: { type: "string" } }
603
613
  }, "sift skill teach <skill-id> --lesson 'when X, do Y'", { required: ["skillId", "lesson", "visibility"] }),
604
- readTool("search.query", "Search authorized brain context and return cited results.", {
614
+ writeTool("skill.feedback", "Report structured feedback for a pinned skill version; external-agent reports remain reported until trusted Sift evidence verifies them.", {
615
+ skillId: { type: "string" },
616
+ skillVersionId: { type: "string" },
617
+ exerciseRef: {
618
+ type: "object",
619
+ properties: { exerciseId: { type: "string" } },
620
+ required: ["exerciseId"]
621
+ },
622
+ subjectRef: { type: "object", properties: {} },
623
+ signalKind: { type: "string" },
624
+ polarity: { type: "string", enum: ["positive", "negative", "mixed", "unknown"] },
625
+ strength: { type: "string", enum: ["weak", "medium", "strong"] },
626
+ payload: { type: "object", properties: {} },
627
+ idempotencyKey: { type: "string" }
628
+ }, "sift skill feedback <skill-id> <skill-version-id>", {
629
+ required: [
630
+ "skillId",
631
+ "skillVersionId",
632
+ "signalKind",
633
+ "polarity",
634
+ "strength",
635
+ "payload",
636
+ "idempotencyKey"
637
+ ],
638
+ idempotency: "required"
639
+ }),
640
+ readTool("search.query", "Search authorized brain context and return raw cited candidate results for exploration.", {
605
641
  query: { type: "string" },
606
642
  limit: { type: "integer", minimum: 1, maximum: 20 }
607
643
  }, "sift search query 'launch risks'"),
608
- readTool("context.assemble", "Assemble compact cited context for an agent.", { query: { type: "string" }, maxChars: { type: "integer", minimum: 1 } }, "sift context assemble 'launch risks'", {
644
+ readTool("context.assemble", "Assemble grounded answer-preparation context with request time, caller identity, task guidance from visible Sift skills when available, safe source metadata, gaps, and raw cited fallback.", {
645
+ query: { type: "string" },
646
+ queryIssuedAt: { type: "string" },
647
+ timezone: { type: "string" },
648
+ maxChars: { type: "integer", minimum: 1 }
649
+ }, "sift context assemble 'launch risks'", {
650
+ required: ["query"],
609
651
  hostedAgent: {
610
652
  toolsets: ["brain", "retrieval"],
611
653
  searchTerms: ["context", "cite", "answer", "evidence"]
612
654
  }
613
655
  }),
656
+ hostedAgentOnlyReadTool("web.search", "Search public web sources for current or public facts.", {
657
+ query: {
658
+ type: "string",
659
+ description: "Public web search query. Do not include private Sift brain context unless the user explicitly provided it for public lookup."
660
+ },
661
+ limit: { type: "integer", minimum: 1, maximum: 10 },
662
+ recencyDays: { type: "integer", minimum: 1, maximum: 3650 },
663
+ allowedDomains: { type: "array", items: { type: "string" } },
664
+ blockedDomains: { type: "array", items: { type: "string" } }
665
+ }, {
666
+ required: ["query"],
667
+ hostedAgent: {
668
+ toolsets: ["web"],
669
+ searchTerms: [
670
+ "web",
671
+ "search",
672
+ "current",
673
+ "public",
674
+ "company",
675
+ "product",
676
+ "docs",
677
+ "news",
678
+ "pricing",
679
+ "people",
680
+ "law",
681
+ "rules"
682
+ ],
683
+ inputHints: ["query", "limit", "recencyDays", "allowedDomains", "blockedDomains"],
684
+ riskClass: "medium"
685
+ }
686
+ }),
687
+ hostedAgentOnlyReadTool("web.fetch", "Read one selected public URL through guarded bounded extraction.", {
688
+ url: {
689
+ type: "string",
690
+ description: "Public http(s) URL to fetch. Local, private, and metadata URLs are refused."
691
+ },
692
+ maxChars: { type: "integer", minimum: 1, maximum: 12e3 }
693
+ }, {
694
+ required: ["url"],
695
+ hostedAgent: {
696
+ toolsets: ["web"],
697
+ searchTerms: ["web", "fetch", "read", "url", "page", "extract", "public"],
698
+ inputHints: ["url", "maxChars"],
699
+ riskClass: "medium"
700
+ }
701
+ }),
614
702
  readTool("context.profile", "Read a permission-filtered profile context model.", {}, "sift context profile"),
615
703
  readTool("evidence.list", "List authorized evidence links for a record.", stringProps(["recordId"]), "sift evidence list <record-id>"),
616
704
  readTool("evidence.get", "Read an authorized evidence item.", stringProps(["evidenceId"]), "sift evidence get <evidence-id>"),
@@ -713,6 +801,7 @@ var init_discovery = __esm({
713
801
  "use strict";
714
802
  init_registry();
715
803
  IMPLEMENTED_TOOL_NAMES = [
804
+ "contract.get",
716
805
  "whoami",
717
806
  "brain.list",
718
807
  "brain.use",
@@ -751,24 +840,24 @@ var init_discovery = __esm({
751
840
  });
752
841
 
753
842
  // ../tools/dist/results.js
754
- function captureResult(result2) {
843
+ function captureResult(result) {
755
844
  return {
756
- status: result2.job?.status ?? result2.status ?? "captured",
757
- jobId: result2.job?.id,
758
- sourceId: result2.sourceId,
759
- sourceItemId: result2.sourceItemId,
760
- recordId: result2.recordId,
761
- versionId: result2.versionId,
762
- versionNumber: result2.versionNumber
845
+ status: result.job?.status ?? result.status ?? "captured",
846
+ jobId: result.job?.id,
847
+ sourceId: result.sourceId,
848
+ sourceItemId: result.sourceItemId,
849
+ recordId: result.recordId,
850
+ versionId: result.versionId,
851
+ versionNumber: result.versionNumber
763
852
  };
764
853
  }
765
- function workRecordResult(result2) {
854
+ function workRecordResult(result) {
766
855
  return {
767
- status: result2.job.status,
768
- jobId: result2.job.id,
769
- recordId: result2.recordId,
770
- versionId: result2.versionId,
771
- versionNumber: result2.versionNumber
856
+ status: result.job.status,
857
+ jobId: result.job.id,
858
+ recordId: result.recordId,
859
+ versionId: result.versionId,
860
+ versionNumber: result.versionNumber
772
861
  };
773
862
  }
774
863
  var init_results = __esm({
@@ -780,16 +869,16 @@ var init_results = __esm({
780
869
  // ../tools/dist/captureTools.js
781
870
  async function executeCaptureText(input, toolInput) {
782
871
  const capture = parseCaptureText(toolInput);
783
- const result2 = await input.service.ingestText({ auth: input.auth, ...capture });
784
- return captureResult(result2);
872
+ const result = await input.service.ingestText({ auth: input.auth, ...capture });
873
+ return captureResult(result);
785
874
  }
786
875
  async function executeCaptureFile(input, toolInput) {
787
876
  if (input.service.ingestFile === void 0) {
788
877
  throw new Error("Tool 'capture.file' requires a file ingestion service contract.");
789
878
  }
790
879
  const file = parseCaptureFile(toolInput);
791
- const result2 = await input.service.ingestFile({ auth: input.auth, ...file });
792
- return captureResult(result2);
880
+ const result = await input.service.ingestFile({ auth: input.auth, ...file });
881
+ return captureResult(result);
793
882
  }
794
883
  async function executeCaptureBatch(input, toolInput) {
795
884
  const batch = parseCaptureBatch(toolInput);
@@ -797,16 +886,16 @@ async function executeCaptureBatch(input, toolInput) {
797
886
  for (const item of batch.items) {
798
887
  if (item.kind === "text") {
799
888
  const { kind: _kind2, ...capture } = item;
800
- const result3 = await input.service.ingestText({ auth: input.auth, ...capture });
801
- results.push(captureResult(result3));
889
+ const result2 = await input.service.ingestText({ auth: input.auth, ...capture });
890
+ results.push(captureResult(result2));
802
891
  continue;
803
892
  }
804
893
  if (input.service.ingestFile === void 0) {
805
894
  throw new Error("Tool 'capture.batch' requires a file ingestion service contract.");
806
895
  }
807
896
  const { kind: _kind, ...file } = item;
808
- const result2 = await input.service.ingestFile({ auth: input.auth, ...file });
809
- results.push(captureResult(result2));
897
+ const result = await input.service.ingestFile({ auth: input.auth, ...file });
898
+ results.push(captureResult(result));
810
899
  }
811
900
  return results;
812
901
  }
@@ -879,6 +968,15 @@ function skillToolHandlers(input, toolInput) {
879
968
  if (input.service.teachSkill === void 0)
880
969
  throw missingSkillService("skill.teach");
881
970
  return input.service.teachSkill({ auth: input.auth, ...parseSkillTeach(toolInput) });
971
+ },
972
+ "skill.feedback": () => {
973
+ if (input.service.reportFeedbackSignal === void 0) {
974
+ throw missingSkillService("skill.feedback");
975
+ }
976
+ return input.service.reportFeedbackSignal({
977
+ auth: input.auth,
978
+ ...parseSkillFeedback(toolInput)
979
+ });
882
980
  }
883
981
  };
884
982
  }
@@ -888,7 +986,8 @@ function skillToolAvailability(service) {
888
986
  [service.getSkill !== void 0, ["skill.get"]],
889
987
  [service.getSkillFile !== void 0, ["skill.file"]],
890
988
  [service.recordSkillExercise !== void 0, ["skill.exercise"]],
891
- [service.teachSkill !== void 0, ["skill.teach"]]
989
+ [service.teachSkill !== void 0, ["skill.teach"]],
990
+ [service.reportFeedbackSignal !== void 0, ["skill.feedback"]]
892
991
  ];
893
992
  }
894
993
  function parseSkillExercise(input) {
@@ -921,6 +1020,29 @@ function parseSkillTeach(input) {
921
1020
  visibility
922
1021
  };
923
1022
  }
1023
+ function parseSkillFeedback(input) {
1024
+ const exerciseRef = optionalObject(input, "exerciseRef");
1025
+ const subjectRef = optionalObject(input, "subjectRef");
1026
+ const polarity = requireString(input, "polarity");
1027
+ const strength = requireString(input, "strength");
1028
+ if (!isSkillFeedbackPolarity(polarity)) {
1029
+ throw new Error("polarity must be positive, negative, mixed, or unknown.");
1030
+ }
1031
+ if (!isSkillFeedbackStrength(strength)) {
1032
+ throw new Error("strength must be weak, medium, or strong.");
1033
+ }
1034
+ return {
1035
+ skillId: requireString(input, "skillId"),
1036
+ skillVersionId: requireString(input, "skillVersionId"),
1037
+ exerciseRef: exerciseRef === void 0 ? void 0 : { exerciseId: requireString(exerciseRef, "exerciseId") },
1038
+ subjectRef,
1039
+ signalKind: requireString(input, "signalKind"),
1040
+ polarity,
1041
+ strength,
1042
+ payload: optionalObject(input, "payload") ?? {},
1043
+ idempotencyKey: requireString(input, "idempotencyKey")
1044
+ };
1045
+ }
924
1046
  function requireSkillOutputRef(input, key) {
925
1047
  const value = input[key];
926
1048
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -939,6 +1061,22 @@ function requireSkillOutputRef(input, key) {
939
1061
  }
940
1062
  throw new Error(`${key}.kind must be record or external.`);
941
1063
  }
1064
+ function optionalObject(input, key) {
1065
+ const value = input[key];
1066
+ if (value === void 0) {
1067
+ return void 0;
1068
+ }
1069
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1070
+ throw new Error(`${key} must be an object.`);
1071
+ }
1072
+ return value;
1073
+ }
1074
+ function isSkillFeedbackPolarity(value) {
1075
+ return value === "positive" || value === "negative" || value === "mixed" || value === "unknown";
1076
+ }
1077
+ function isSkillFeedbackStrength(value) {
1078
+ return value === "weak" || value === "medium" || value === "strong";
1079
+ }
942
1080
  function missingSkillService(toolName) {
943
1081
  return new Error(`Tool '${toolName}' requires a runtime service contract.`);
944
1082
  }
@@ -1034,7 +1172,9 @@ function runtimeAvailableToolNames(service) {
1034
1172
  [service.listGraphNeighbors !== void 0, ["graph.neighbors"]],
1035
1173
  [service.listEvents !== void 0, ["event.list"]],
1036
1174
  [service.getContextProfile !== void 0, ["context.profile"]],
1037
- [service.listAuditEvents !== void 0, ["audit.events"]]
1175
+ [service.listAuditEvents !== void 0, ["audit.events"]],
1176
+ [service.webSearch !== void 0, ["web.search"]],
1177
+ [service.webFetch !== void 0, ["web.fetch"]]
1038
1178
  ];
1039
1179
  return [...baseNames, ...optionalNames.flatMap(([enabled, names]) => enabled ? names : [])];
1040
1180
  }
@@ -1112,6 +1252,73 @@ var init_toolLog = __esm({
1112
1252
  }
1113
1253
  });
1114
1254
 
1255
+ // ../tools/dist/webToolRuntime.js
1256
+ function webToolHandlers(input, toolInput) {
1257
+ return {
1258
+ "web.search": () => executeWebSearch(input, toolInput),
1259
+ "web.fetch": () => executeWebFetch(input, toolInput)
1260
+ };
1261
+ }
1262
+ function executeWebSearch(input, toolInput) {
1263
+ if (input.service.webSearch === void 0) {
1264
+ throw new Error("Tool 'web.search' is unavailable without a web search service contract.");
1265
+ }
1266
+ return input.service.webSearch({ auth: input.auth, ...parseWebSearch(toolInput) });
1267
+ }
1268
+ function executeWebFetch(input, toolInput) {
1269
+ if (input.service.webFetch === void 0) {
1270
+ throw new Error("Tool 'web.fetch' is unavailable without a web fetch service contract.");
1271
+ }
1272
+ return input.service.webFetch({ auth: input.auth, ...parseWebFetch(toolInput) });
1273
+ }
1274
+ function parseWebSearch(input) {
1275
+ const parsed = {
1276
+ query: requireString(input, "query"),
1277
+ limit: requireBoundedInteger(input, "limit", 5, 1, 10)
1278
+ };
1279
+ if (input.recencyDays !== void 0) {
1280
+ parsed.recencyDays = requireBoundedInteger(input, "recencyDays", 30, 1, 3650);
1281
+ }
1282
+ if (input.allowedDomains !== void 0) {
1283
+ parsed.allowedDomains = requireBoundedStringArray(input, "allowedDomains", 10);
1284
+ }
1285
+ if (input.blockedDomains !== void 0) {
1286
+ parsed.blockedDomains = requireBoundedStringArray(input, "blockedDomains", 10);
1287
+ }
1288
+ return parsed;
1289
+ }
1290
+ function parseWebFetch(input) {
1291
+ return {
1292
+ url: requireString(input, "url"),
1293
+ maxChars: requireBoundedInteger(input, "maxChars", 8e3, 1, 12e3)
1294
+ };
1295
+ }
1296
+ function requireBoundedStringArray(input, key, maxItems) {
1297
+ const value = input[key];
1298
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
1299
+ throw new Error(`${key} must be a string array.`);
1300
+ }
1301
+ const values = value.map((item) => item.trim()).filter((item) => item.length > 0);
1302
+ if (values.length > maxItems) {
1303
+ throw new Error(`${key} must contain no more than ${maxItems} items.`);
1304
+ }
1305
+ return values;
1306
+ }
1307
+ function requireBoundedInteger(input, key, fallback, minimum, maximum) {
1308
+ const value = input[key];
1309
+ const integer = value === void 0 ? fallback : value;
1310
+ if (!Number.isInteger(integer) || Number(integer) < minimum || Number(integer) > maximum) {
1311
+ throw new Error(`${key} must be an integer between ${minimum} and ${maximum}.`);
1312
+ }
1313
+ return Number(integer);
1314
+ }
1315
+ var init_webToolRuntime = __esm({
1316
+ "../tools/dist/webToolRuntime.js"() {
1317
+ "use strict";
1318
+ init_inputParsers();
1319
+ }
1320
+ });
1321
+
1115
1322
  // ../tools/dist/executor.js
1116
1323
  function createRuntimeToolExecutor(input) {
1117
1324
  const availableToolNames = runtimeAvailableToolNames(input.service);
@@ -1136,7 +1343,7 @@ function createRuntimeToolExecutor(input) {
1136
1343
  throw error;
1137
1344
  }
1138
1345
  try {
1139
- const result2 = await handler();
1346
+ const result = await handler();
1140
1347
  logToolCall({
1141
1348
  auth: input.auth,
1142
1349
  onToolLog: input.onToolLog,
@@ -1144,9 +1351,9 @@ function createRuntimeToolExecutor(input) {
1144
1351
  toolInput,
1145
1352
  status: "success",
1146
1353
  startedAt,
1147
- result: result2
1354
+ result
1148
1355
  });
1149
- return result2;
1356
+ return result;
1150
1357
  } catch (error) {
1151
1358
  logToolCall({
1152
1359
  auth: input.auth,
@@ -1179,6 +1386,7 @@ function createToolHandlers(input, toolInput) {
1179
1386
  "context.profile": () => executeContextProfile(input, toolInput),
1180
1387
  "decision.create": () => executeDecisionCreate(input, toolInput),
1181
1388
  "task.create": () => executeTaskCreate(input, toolInput),
1389
+ ...webToolHandlers(input, toolInput),
1182
1390
  ...skillToolHandlers(input, toolInput),
1183
1391
  ...agentIdentityToolHandlers(input, toolInput),
1184
1392
  ...contractToolHandlers(input),
@@ -1238,6 +1446,21 @@ function executeSearchQuery(input, toolInput) {
1238
1446
  }
1239
1447
  function executeContextAssemble(input, toolInput) {
1240
1448
  const query = parseContextQuery(toolInput);
1449
+ if (input.service.assembleGroundedContext !== void 0) {
1450
+ return input.service.assembleGroundedContext({
1451
+ auth: input.auth,
1452
+ query: query.query,
1453
+ queryIssuedAt: query.queryIssuedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1454
+ timezone: query.timezone ?? "UTC",
1455
+ requester: {
1456
+ principalId: input.auth.principalId,
1457
+ actorId: input.auth.actorId
1458
+ },
1459
+ surface: taskGuidanceSurface(input.transport),
1460
+ limit: 8,
1461
+ maxChars: query.maxChars
1462
+ });
1463
+ }
1241
1464
  return input.service.retrieveCitedContext({
1242
1465
  auth: input.auth,
1243
1466
  query: query.query,
@@ -1245,6 +1468,13 @@ function executeContextAssemble(input, toolInput) {
1245
1468
  maxChars: query.maxChars
1246
1469
  });
1247
1470
  }
1471
+ function taskGuidanceSurface(transport) {
1472
+ if (transport === "cli")
1473
+ return "cli";
1474
+ if (transport === "hosted_mcp" || transport === "local_mcp")
1475
+ return "mcp";
1476
+ return "app";
1477
+ }
1248
1478
  function executeContextProfile(input, toolInput) {
1249
1479
  if (input.service.getContextProfile === void 0) {
1250
1480
  throw new Error("Tool 'context.profile' requires a profile read service contract.");
@@ -1257,16 +1487,16 @@ async function executeDecisionCreate(input, toolInput) {
1257
1487
  throw new Error("Tool 'decision.create' requires a work-record service contract.");
1258
1488
  }
1259
1489
  const decision = parseDecision(toolInput);
1260
- const result2 = await input.service.createDecision({ auth: input.auth, ...decision });
1261
- return workRecordResult(result2);
1490
+ const result = await input.service.createDecision({ auth: input.auth, ...decision });
1491
+ return workRecordResult(result);
1262
1492
  }
1263
1493
  async function executeTaskCreate(input, toolInput) {
1264
1494
  if (input.service.createTask === void 0) {
1265
1495
  throw new Error("Tool 'task.create' requires a work-record service contract.");
1266
1496
  }
1267
1497
  const task = parseTask(toolInput);
1268
- const result2 = await input.service.createTask({ auth: input.auth, ...task });
1269
- return workRecordResult(result2);
1498
+ const result = await input.service.createTask({ auth: input.auth, ...task });
1499
+ return workRecordResult(result);
1270
1500
  }
1271
1501
  function executeSourceList(input) {
1272
1502
  if (input.service.listSources === void 0) {
@@ -1317,16 +1547,16 @@ async function executeRecordCreateMarkdown(input, toolInput) {
1317
1547
  throw new Error("Tool 'record.create_markdown' requires a record write service contract.");
1318
1548
  }
1319
1549
  const record = parseMarkdownRecord(toolInput);
1320
- const result2 = await input.service.createMarkdownRecord({ auth: input.auth, ...record });
1321
- return workRecordResult(result2);
1550
+ const result = await input.service.createMarkdownRecord({ auth: input.auth, ...record });
1551
+ return workRecordResult(result);
1322
1552
  }
1323
1553
  async function executeRecordPatchSection(input, toolInput) {
1324
1554
  if (input.service.patchRecordSection === void 0) {
1325
1555
  throw new Error("Tool 'record.patch_section' requires a record write service contract.");
1326
1556
  }
1327
1557
  const patch = parseRecordSectionPatch(toolInput);
1328
- const result2 = await input.service.patchRecordSection({ auth: input.auth, ...patch });
1329
- return workRecordResult(result2);
1558
+ const result = await input.service.patchRecordSection({ auth: input.auth, ...patch });
1559
+ return workRecordResult(result);
1330
1560
  }
1331
1561
  function executeRecordVersions(input, toolInput) {
1332
1562
  if (input.service.listRecordVersions === void 0) {
@@ -1356,6 +1586,7 @@ var init_executor = __esm({
1356
1586
  init_toolAvailability();
1357
1587
  init_results();
1358
1588
  init_toolLog();
1589
+ init_webToolRuntime();
1359
1590
  }
1360
1591
  });
1361
1592
 
@@ -1394,7 +1625,7 @@ function createMcpAdapter(input) {
1394
1625
  ...IMPLEMENTED_TOOL_NAMES
1395
1626
  ];
1396
1627
  const availableNameSet = new Set(availableToolNames);
1397
- const available = listToolDefinitions().filter((tool) => availableNameSet.has(tool.name) && tool.transports.includes(input.transport) && input.capabilities.includes(tool.capability));
1628
+ const available = listToolDefinitions().filter((tool) => availableNameSet.has(tool.name) && tool.transports.includes(input.transport) && isToolAuthorized(input.capabilities, tool));
1398
1629
  return {
1399
1630
  listTools() {
1400
1631
  return createMcpToolSchemas({
@@ -1408,11 +1639,11 @@ function createMcpAdapter(input) {
1408
1639
  return errorResult2("tool_unavailable", `Tool '${call.name}' is unavailable for this MCP transport or scope.`);
1409
1640
  }
1410
1641
  try {
1411
- const result2 = await input.executor.execute(call.name, call.arguments);
1642
+ const result = await input.executor.execute(call.name, call.arguments);
1412
1643
  return {
1413
1644
  isError: false,
1414
- structuredContent: result2,
1415
- content: [{ type: "text", text: renderToolResult(result2) }]
1645
+ structuredContent: result,
1646
+ content: [{ type: "text", text: renderToolResult(result) }]
1416
1647
  };
1417
1648
  } catch (error) {
1418
1649
  return errorResult2(classifyToolError(error), messageForError(error));
@@ -1420,14 +1651,14 @@ function createMcpAdapter(input) {
1420
1651
  }
1421
1652
  };
1422
1653
  }
1423
- function renderToolResult(result2) {
1424
- if (typeof result2 === "object" && result2 !== null && "contextMarkdown" in result2) {
1425
- const context = result2;
1654
+ function renderToolResult(result) {
1655
+ if (typeof result === "object" && result !== null && "contextMarkdown" in result) {
1656
+ const context = result;
1426
1657
  if (typeof context.contextMarkdown === "string") {
1427
1658
  return context.contextMarkdown;
1428
1659
  }
1429
1660
  }
1430
- return JSON.stringify(result2);
1661
+ return JSON.stringify(result);
1431
1662
  }
1432
1663
  function classifyToolError(error) {
1433
1664
  if (error instanceof Error) {
@@ -1546,89 +1777,41 @@ var init_hostedMcpEntrypoint = __esm({
1546
1777
  }
1547
1778
  });
1548
1779
 
1549
- // ../tools/dist/localMcpStdioServer.js
1550
- function createLocalMcpStdioServer(input) {
1780
+ // ../tools/dist/mcpJsonRpcCore.js
1781
+ function createMcpJsonRpcCore(input) {
1782
+ const { adapter, config } = input;
1551
1783
  return {
1552
- async serve(serverInput) {
1553
- const adapter = createMcpAdapter({
1554
- transport: "local_mcp",
1555
- capabilities: serverInput.capabilities,
1556
- executor: serverInput.executor
1557
- });
1558
- let buffer = "";
1559
- input.input.setEncoding("utf8");
1560
- for await (const chunk of input.input) {
1561
- buffer += chunk;
1562
- let newline = buffer.indexOf("\n");
1563
- while (newline >= 0) {
1564
- const line = buffer.slice(0, newline).trim();
1565
- buffer = buffer.slice(newline + 1);
1566
- if (line.length > 0) {
1567
- await handleLine(line, adapter, input.output, input.error);
1568
- }
1569
- newline = buffer.indexOf("\n");
1570
- }
1784
+ async handleMessage(message) {
1785
+ if (message.id === void 0) {
1786
+ return null;
1571
1787
  }
1572
- const trailing = buffer.trim();
1573
- if (trailing.length > 0) {
1574
- await handleLine(trailing, adapter, input.output, input.error);
1788
+ const id = normalizeId(message.id);
1789
+ if (message.jsonrpc !== "2.0" || typeof message.method !== "string") {
1790
+ return errorResponse(id, -32600, "Invalid Request");
1791
+ }
1792
+ try {
1793
+ return {
1794
+ jsonrpc: "2.0",
1795
+ id,
1796
+ result: await dispatchRequest(message.method, message.params, adapter, config)
1797
+ };
1798
+ } catch (err) {
1799
+ return errorResponse(id, -32601, err instanceof Error ? err.message : "Method not found");
1575
1800
  }
1576
1801
  }
1577
1802
  };
1578
1803
  }
1579
- async function handleLine(line, adapter, output, error) {
1580
- let message;
1581
- try {
1582
- message = JSON.parse(line);
1583
- } catch {
1584
- writeResponse(output, {
1585
- jsonrpc: "2.0",
1586
- id: null,
1587
- error: { code: -32700, message: "Parse error" }
1588
- });
1589
- return;
1590
- }
1591
- if (message.id === void 0) {
1592
- if (message.method === "notifications/initialized")
1593
- return;
1594
- error?.write(`Ignoring MCP notification '${String(message.method)}'.
1595
- `);
1596
- return;
1597
- }
1598
- const id = normalizeId(message.id);
1599
- if (message.jsonrpc !== "2.0" || typeof message.method !== "string") {
1600
- writeResponse(output, {
1601
- jsonrpc: "2.0",
1602
- id,
1603
- error: { code: -32600, message: "Invalid Request" }
1604
- });
1605
- return;
1606
- }
1607
- try {
1608
- writeResponse(output, {
1609
- jsonrpc: "2.0",
1610
- id,
1611
- result: await dispatchRequest(message.method, message.params, adapter)
1612
- });
1613
- } catch (err) {
1614
- writeResponse(output, {
1615
- jsonrpc: "2.0",
1616
- id,
1617
- error: {
1618
- code: -32601,
1619
- message: err instanceof Error ? err.message : "Method not found"
1620
- }
1621
- });
1622
- }
1804
+ function parseErrorResponse() {
1805
+ return errorResponse(null, -32700, "Parse error");
1623
1806
  }
1624
- async function dispatchRequest(method, params, adapter) {
1807
+ async function dispatchRequest(method, params, adapter, config) {
1625
1808
  if (method === "initialize") {
1626
1809
  const requested = readProtocolVersion(params);
1627
1810
  return {
1628
1811
  protocolVersion: requested ?? MCP_PROTOCOL_VERSION,
1629
1812
  capabilities: { tools: { listChanged: false } },
1630
- serverInfo: { name: "sift-local-mcp", version: "0.1.0" },
1631
- instructions: "Call contract.get first and echo its contractVersion on every other Sift tool call. Use Sift tools to read and write the hosted canonical brain."
1813
+ serverInfo: { name: config.serverName, version: config.version },
1814
+ instructions: config.instructions
1632
1815
  };
1633
1816
  }
1634
1817
  if (method === "ping")
@@ -1639,23 +1822,26 @@ async function dispatchRequest(method, params, adapter) {
1639
1822
  const call = parseToolCall(params);
1640
1823
  return adapter.callTool(call);
1641
1824
  }
1642
- throw new Error(`Method '${method}' is not supported by Sift local MCP.`);
1825
+ throw new Error(`Method '${method}' is not supported by Sift MCP.`);
1643
1826
  }
1644
1827
  function readProtocolVersion(params) {
1645
- if (!isRecord(params))
1828
+ if (!isRecord2(params))
1646
1829
  return void 0;
1647
1830
  return typeof params.protocolVersion === "string" ? params.protocolVersion : void 0;
1648
1831
  }
1649
1832
  function parseToolCall(params) {
1650
- if (!isRecord(params) || typeof params.name !== "string") {
1833
+ if (!isRecord2(params) || typeof params.name !== "string") {
1651
1834
  throw new Error("tools/call requires a tool name.");
1652
1835
  }
1653
1836
  return {
1654
1837
  name: params.name,
1655
- arguments: isRecord(params.arguments) ? params.arguments : {}
1838
+ arguments: isRecord2(params.arguments) ? params.arguments : {}
1656
1839
  };
1657
1840
  }
1658
- function isRecord(value) {
1841
+ function errorResponse(id, code, message) {
1842
+ return { jsonrpc: "2.0", id, error: { code, message } };
1843
+ }
1844
+ function isRecord2(value) {
1659
1845
  return typeof value === "object" && value !== null && !Array.isArray(value);
1660
1846
  }
1661
1847
  function normalizeId(value) {
@@ -1663,16 +1849,80 @@ function normalizeId(value) {
1663
1849
  return value;
1664
1850
  return null;
1665
1851
  }
1852
+ var MCP_PROTOCOL_VERSION;
1853
+ var init_mcpJsonRpcCore = __esm({
1854
+ "../tools/dist/mcpJsonRpcCore.js"() {
1855
+ "use strict";
1856
+ MCP_PROTOCOL_VERSION = "2025-11-25";
1857
+ }
1858
+ });
1859
+
1860
+ // ../tools/dist/localMcpStdioServer.js
1861
+ function createLocalMcpStdioServer(input) {
1862
+ return {
1863
+ async serve(serverInput) {
1864
+ const adapter = createMcpAdapter({
1865
+ transport: "local_mcp",
1866
+ capabilities: serverInput.capabilities,
1867
+ executor: serverInput.executor
1868
+ });
1869
+ const core = createMcpJsonRpcCore({
1870
+ adapter,
1871
+ config: {
1872
+ serverName: "sift-local-mcp",
1873
+ version: "0.1.0",
1874
+ instructions: LOCAL_INSTRUCTIONS
1875
+ }
1876
+ });
1877
+ let buffer = "";
1878
+ input.input.setEncoding("utf8");
1879
+ for await (const chunk of input.input) {
1880
+ buffer += chunk;
1881
+ let newline = buffer.indexOf("\n");
1882
+ while (newline >= 0) {
1883
+ const line = buffer.slice(0, newline).trim();
1884
+ buffer = buffer.slice(newline + 1);
1885
+ if (line.length > 0) {
1886
+ await handleLine(line, core, input.output, input.error);
1887
+ }
1888
+ newline = buffer.indexOf("\n");
1889
+ }
1890
+ }
1891
+ const trailing = buffer.trim();
1892
+ if (trailing.length > 0) {
1893
+ await handleLine(trailing, core, input.output, input.error);
1894
+ }
1895
+ }
1896
+ };
1897
+ }
1898
+ async function handleLine(line, core, output, error) {
1899
+ let message;
1900
+ try {
1901
+ message = JSON.parse(line);
1902
+ } catch {
1903
+ writeResponse(output, parseErrorResponse());
1904
+ return;
1905
+ }
1906
+ if (message.id === void 0 && message.method !== "notifications/initialized") {
1907
+ error?.write(`Ignoring MCP notification '${String(message.method)}'.
1908
+ `);
1909
+ }
1910
+ const response = await core.handleMessage(message);
1911
+ if (response !== null) {
1912
+ writeResponse(output, response);
1913
+ }
1914
+ }
1666
1915
  function writeResponse(output, response) {
1667
1916
  output.write(`${JSON.stringify(response)}
1668
1917
  `);
1669
1918
  }
1670
- var MCP_PROTOCOL_VERSION;
1919
+ var LOCAL_INSTRUCTIONS;
1671
1920
  var init_localMcpStdioServer = __esm({
1672
1921
  "../tools/dist/localMcpStdioServer.js"() {
1673
1922
  "use strict";
1674
1923
  init_mcpAdapter();
1675
- MCP_PROTOCOL_VERSION = "2025-11-25";
1924
+ init_mcpJsonRpcCore();
1925
+ LOCAL_INSTRUCTIONS = "Call contract.get first and echo its contractVersion on every other Sift tool call. Use Sift tools to read and write the hosted canonical brain.";
1676
1926
  }
1677
1927
  });
1678
1928
 
@@ -1686,11 +1936,13 @@ __export(dist_exports, {
1686
1936
  createHostedMcpEntrypoint: () => createHostedMcpEntrypoint,
1687
1937
  createLocalMcpStdioServer: () => createLocalMcpStdioServer,
1688
1938
  createMcpAdapter: () => createMcpAdapter,
1939
+ createMcpJsonRpcCore: () => createMcpJsonRpcCore,
1689
1940
  createMcpToolSchemas: () => createMcpToolSchemas,
1690
1941
  createRuntimeToolExecutor: () => createRuntimeToolExecutor,
1691
1942
  isGatedTool: () => isGatedTool,
1692
1943
  isToolAuthorized: () => isToolAuthorized,
1693
- listToolDefinitions: () => listToolDefinitions
1944
+ listToolDefinitions: () => listToolDefinitions,
1945
+ parseErrorResponse: () => parseErrorResponse
1694
1946
  });
1695
1947
  var init_dist = __esm({
1696
1948
  "../tools/dist/index.js"() {
@@ -1700,13 +1952,14 @@ var init_dist = __esm({
1700
1952
  init_hostedMcpEntrypoint();
1701
1953
  init_localMcpStdioServer();
1702
1954
  init_mcpAdapter();
1955
+ init_mcpJsonRpcCore();
1703
1956
  init_gating();
1704
1957
  init_registry();
1705
1958
  }
1706
1959
  });
1707
1960
 
1708
1961
  // src/index.ts
1709
- import { readFile } from "fs/promises";
1962
+ import { readFile as readFile2 } from "fs/promises";
1710
1963
 
1711
1964
  // src/support.ts
1712
1965
  import { createHash } from "crypto";
@@ -1723,19 +1976,19 @@ function renderScope(scope) {
1723
1976
  ""
1724
1977
  ].join("\n");
1725
1978
  }
1726
- function validateAuthenticatedScope(config2, now) {
1979
+ function validateAuthenticatedScope(config, now) {
1727
1980
  const requiredScope = [
1728
- ["apiBaseUrl", config2.apiBaseUrl],
1729
- ["workspaceId", config2.workspaceId],
1730
- ["brainId", config2.brainId],
1731
- ["principalId", config2.principalId]
1981
+ ["apiBaseUrl", config.apiBaseUrl],
1982
+ ["workspaceId", config.workspaceId],
1983
+ ["brainId", config.brainId],
1984
+ ["principalId", config.principalId]
1732
1985
  ];
1733
1986
  const missing = requiredScope.find(([, value]) => value.trim().length === 0);
1734
1987
  if (missing !== void 0) {
1735
1988
  throw new Error(`Missing authenticated CLI scope: ${missing[0]}.`);
1736
1989
  }
1737
- if (config2.tokenExpiresAt !== void 0) {
1738
- const expiresAt = Date.parse(config2.tokenExpiresAt);
1990
+ if (config.tokenExpiresAt !== void 0) {
1991
+ const expiresAt = Date.parse(config.tokenExpiresAt);
1739
1992
  if (!Number.isFinite(expiresAt)) {
1740
1993
  throw new Error("Invalid CLI auth expiry timestamp.");
1741
1994
  }
@@ -1744,49 +1997,49 @@ function validateAuthenticatedScope(config2, now) {
1744
1997
  }
1745
1998
  }
1746
1999
  }
1747
- function renderSearchResult(result2) {
1748
- if (typeof result2 === "object" && result2 !== null && "contextMarkdown" in result2) {
1749
- const context = result2;
2000
+ function renderSearchResult(result) {
2001
+ if (typeof result === "object" && result !== null && "contextMarkdown" in result) {
2002
+ const context = result;
1750
2003
  return `${context.contextMarkdown}
1751
2004
  `;
1752
2005
  }
1753
- return `${JSON.stringify(result2)}
2006
+ return `${JSON.stringify(result)}
1754
2007
  `;
1755
2008
  }
1756
- function renderRecordResult(result2) {
1757
- if (typeof result2 === "object" && result2 !== null && "markdown" in result2) {
1758
- const record = result2;
2009
+ function renderRecordResult(result) {
2010
+ if (typeof result === "object" && result !== null && "markdown" in result) {
2011
+ const record = result;
1759
2012
  return `${record.markdown}
1760
2013
  `;
1761
2014
  }
1762
- return `${JSON.stringify(result2)}
2015
+ return `${JSON.stringify(result)}
1763
2016
  `;
1764
2017
  }
1765
- function renderProfileResult(result2) {
1766
- if (typeof result2 === "object" && result2 !== null && "profileMarkdown" in result2) {
1767
- const profile = result2;
2018
+ function renderProfileResult(result) {
2019
+ if (typeof result === "object" && result !== null && "profileMarkdown" in result) {
2020
+ const profile = result;
1768
2021
  return `${profile.profileMarkdown}
1769
2022
  `;
1770
2023
  }
1771
- return `${JSON.stringify(result2)}
2024
+ return `${JSON.stringify(result)}
1772
2025
  `;
1773
2026
  }
1774
2027
  function renderTools(tools) {
1775
2028
  return `${tools.map((tool) => `${tool.name}: ${tool.summary}`).join("\n")}
1776
2029
  `;
1777
2030
  }
1778
- function renderSiftFound(result2) {
2031
+ function renderSiftFound(result) {
1779
2032
  return `Sift found:
1780
2033
 
1781
- ${renderSearchResult(result2)}`;
2034
+ ${renderSearchResult(result)}`;
1782
2035
  }
1783
- function renderWriteReceipt(action, result2) {
1784
- if (typeof result2 !== "object" || result2 === null) {
2036
+ function renderWriteReceipt(action, result) {
2037
+ if (typeof result !== "object" || result === null) {
1785
2038
  return `${action} complete.
1786
- ${JSON.stringify(result2)}
2039
+ ${JSON.stringify(result)}
1787
2040
  `;
1788
2041
  }
1789
- const record = result2;
2042
+ const record = result;
1790
2043
  const lines = [`${action} complete.`];
1791
2044
  addReceiptLine(lines, "Record", record.recordId);
1792
2045
  addReceiptLine(lines, "Version", record.versionId);
@@ -1796,20 +2049,20 @@ ${JSON.stringify(result2)}
1796
2049
  addReceiptLine(lines, "Job", record.job.id);
1797
2050
  }
1798
2051
  if (lines.length === 1) {
1799
- lines.push(JSON.stringify(result2));
2052
+ lines.push(JSON.stringify(result));
1800
2053
  }
1801
2054
  lines.push("");
1802
2055
  return lines.join("\n");
1803
2056
  }
1804
- function renderDoctorResult(result2) {
1805
- return `${result2.checks.map((check) => {
2057
+ function renderDoctorResult(result) {
2058
+ return `${result.checks.map((check) => {
1806
2059
  const fix = check.fix === void 0 ? "" : ` Fix: ${check.fix}`;
1807
2060
  return `[${check.status}] ${check.label}: ${check.detail}${fix}`;
1808
2061
  }).join("\n")}
1809
2062
  `;
1810
2063
  }
1811
- function aliasJson(command, tool, result2) {
1812
- return `${JSON.stringify({ command, tool, result: result2 })}
2064
+ function aliasJson(command, tool, result) {
2065
+ return `${JSON.stringify({ command, tool, result })}
1813
2066
  `;
1814
2067
  }
1815
2068
  function positionalArgs(args) {
@@ -2007,16 +2260,16 @@ async function agentRegister(executor, assertedAgentName, rest, json) {
2007
2260
  if (kind !== void 0) {
2008
2261
  input.kind = kind;
2009
2262
  }
2010
- const result2 = await executor.execute("agent.register", input);
2011
- return ok(json ? `${JSON.stringify(result2)}
2012
- ` : renderAgentRegisterResult(result2));
2263
+ const result = await executor.execute("agent.register", input);
2264
+ return ok(json ? `${JSON.stringify(result)}
2265
+ ` : renderAgentRegisterResult(result));
2013
2266
  }
2014
- function renderAgentRegisterResult(result2) {
2015
- if (typeof result2 !== "object" || result2 === null || !("agent" in result2)) {
2016
- return `${JSON.stringify(result2)}
2267
+ function renderAgentRegisterResult(result) {
2268
+ if (typeof result !== "object" || result === null || !("agent" in result)) {
2269
+ return `${JSON.stringify(result)}
2017
2270
  `;
2018
2271
  }
2019
- const { agent, created, reactivated } = result2;
2272
+ const { agent, created, reactivated } = result;
2020
2273
  const verb = created === true ? "Registered" : reactivated === true ? "Reactivated" : "Already registered";
2021
2274
  const actsFor = agent.actsForDisplayName === void 0 ? "" : ` (acting for ${agent.actsForDisplayName})`;
2022
2275
  return `${verb} agent worker '${agent.name ?? "unknown"}'${actsFor}.
@@ -2085,7 +2338,8 @@ var commandCapabilities = {
2085
2338
  "evidence:get": "record:read",
2086
2339
  "graph:neighbors": "record:read",
2087
2340
  "event:list": "record:read",
2088
- "audit:events": "event:audit:read"
2341
+ "audit:events": "event:audit:read",
2342
+ "roam:import": "source:manage"
2089
2343
  };
2090
2344
  function validateCommandCapability(input) {
2091
2345
  const capability = commandCapabilities[input.commandKey];
@@ -2123,86 +2377,273 @@ function withContractVersion(executor, contractVersion) {
2123
2377
  };
2124
2378
  }
2125
2379
 
2126
- // src/doctor.ts
2127
- async function doctor(input) {
2128
- const checks = [
2129
- { id: "bin", status: "ok", label: "Bin", detail: "sift command entrypoint is configured." },
2130
- { id: "package", status: "ok", label: "Package", detail: "@sift-wiki/cli version unknown, bin sift." },
2131
- authCheck(input.config, input.now),
2132
- scopeCheck(input.config)
2133
- ];
2134
- const apiCheck = await checkApi(input.executor);
2135
- checks.push(apiCheck);
2136
- const availableTools3 = await discoverToolNames(input.executor);
2137
- checks.push(readToolsCheck(availableTools3));
2138
- checks.push(writeToolsCheck(input.config, availableTools3));
2139
- checks.push(recordGetCheck(availableTools3));
2140
- const result2 = {
2141
- ok: !checks.some((check) => check.status === "failed"),
2142
- apiBaseUrl: input.config.apiBaseUrl.trim().length > 0 ? input.config.apiBaseUrl : void 0,
2143
- scope: scopeResult(input.config),
2144
- checks
2145
- };
2146
- return {
2147
- exitCode: result2.ok ? 0 : 1,
2148
- stdout: input.json ? `${JSON.stringify(result2)}
2149
- ` : renderDoctorResult(result2),
2150
- stderr: ""
2151
- };
2152
- }
2153
- async function discoverToolNames(executor) {
2154
- if (executor === void 0) return void 0;
2155
- if (executor.listAvailableToolNames !== void 0) return executor.listAvailableToolNames();
2156
- const result2 = await executor.execute("tools.list", {});
2157
- return toolNamesFromResult(result2);
2158
- }
2159
- async function apiReachability(executor) {
2160
- if (executor === void 0) return { reachable: false, detail: "No API executor is configured." };
2380
+ // src/roamImport.ts
2381
+ async function runRoamImportCommand(input) {
2161
2382
  try {
2162
- await executor.execute("whoami", {});
2163
- return { reachable: true, detail: "whoami succeeded." };
2383
+ const parsed = parseOptions(input.rest);
2384
+ const scope = parseCliRoamScope(optionalOption(parsed, "scope") ?? "sift-tag");
2385
+ const mode = parseCliRoamMode(optionalOption(parsed, "mode") ?? "personal");
2386
+ const limit = parseIntegerOption(parsed, "limit", 100);
2387
+ if (limit < 1 || limit > 100) {
2388
+ throw new Error("Option --limit must be between 1 and 100.");
2389
+ }
2390
+ const wholeGraphConfirmed = input.rest.includes("--confirm-whole-graph") || input.rest.includes("--yes");
2391
+ if (scope === "whole_graph" && !wholeGraphConfirmed) {
2392
+ throw new Error("Option --confirm-whole-graph is required.");
2393
+ }
2394
+ const workspaceAttestation = input.rest.includes("--workspace-attestation") || input.rest.includes("--confirm-workspace");
2395
+ if (mode === "workspace" && !workspaceAttestation) {
2396
+ throw new Error("Option --workspace-attestation is required.");
2397
+ }
2398
+ if (input.reader === void 0) {
2399
+ throw new Error(
2400
+ "Roam import needs the local Roam helper. Run this command through the Sift CLI package."
2401
+ );
2402
+ }
2403
+ if (input.importer === void 0) {
2404
+ throw new Error("Not signed in. Run 'sift login', then retry 'sift roam import'.");
2405
+ }
2406
+ const records = await input.reader.exportPages({
2407
+ scope,
2408
+ graph: optionalOption(parsed, "graph"),
2409
+ limit,
2410
+ now: input.now
2411
+ });
2412
+ if (records.length === 0) {
2413
+ throw new Error(
2414
+ scope === "sift_tag" ? "No Roam pages marked [[Sift]] were found." : "No Roam pages were found for import."
2415
+ );
2416
+ }
2417
+ const result = await input.importer.importRecords({
2418
+ mode,
2419
+ scope,
2420
+ records,
2421
+ defaultVisibility: visibilityOption(parsed),
2422
+ workspaceAttestation,
2423
+ wholeGraphConfirmed
2424
+ });
2425
+ return ok(input.json ? `${JSON.stringify(result)}
2426
+ ` : renderRoamImportResult(result));
2164
2427
  } catch (error) {
2165
- return {
2166
- reachable: false,
2167
- detail: error instanceof Error ? error.message : "Hosted Sift API request failed."
2168
- };
2428
+ return errorResult(error, input.json);
2169
2429
  }
2170
2430
  }
2171
- function scopeResult(config2) {
2172
- if (config2.workspaceId.trim().length === 0 || config2.brainId.trim().length === 0 || config2.principalId.trim().length === 0) {
2173
- return void 0;
2174
- }
2431
+ function createSiftRoamImportClient(input) {
2432
+ const fetchImpl = input.fetch ?? globalThis.fetch;
2175
2433
  return {
2176
- workspaceId: config2.workspaceId,
2177
- brainId: config2.brainId,
2178
- principalId: config2.principalId,
2179
- capabilities: [...config2.capabilities]
2434
+ async importRecords(request) {
2435
+ const response = await fetchImpl(roamImportUrl(input.apiBaseUrl, input.workspaceId), {
2436
+ method: "POST",
2437
+ headers: {
2438
+ Authorization: `Bearer ${input.token}`,
2439
+ "Content-Type": "application/json"
2440
+ },
2441
+ body: JSON.stringify(request)
2442
+ });
2443
+ const body = await response.text();
2444
+ const parsed = body.length > 0 ? parseJson(body) : {};
2445
+ if (!response.ok) {
2446
+ throw new Error(responseError(parsed, response.status));
2447
+ }
2448
+ return parseRoamImportResult(parsed);
2449
+ }
2180
2450
  };
2181
2451
  }
2182
- async function checkApi(executor) {
2183
- if (executor === void 0) {
2184
- return {
2185
- id: "api",
2186
- status: "warning",
2187
- label: "API",
2188
- detail: "not checked because no API executor is configured.",
2189
- fix: "Run sift login or configure hosted API credentials."
2190
- };
2452
+ function parseCliRoamScope(value) {
2453
+ if (value === "sift-tag" || value === "sift_tag" || value === "marked" || value === "sift") {
2454
+ return "sift_tag";
2191
2455
  }
2192
- const api = await apiReachability(executor);
2193
- return api.reachable ? { id: "api", status: "ok", label: "API", detail: "whoami succeeded." } : { id: "api", status: "failed", label: "API", detail: api.detail };
2456
+ if (value === "whole-graph" || value === "whole_graph" || value === "everything") {
2457
+ return "whole_graph";
2458
+ }
2459
+ throw new Error("Roam scope must be sift-tag or whole-graph.");
2194
2460
  }
2195
- function authCheck(config2, now) {
2196
- if (config2.apiBaseUrl.trim().length === 0 || config2.workspaceId.trim().length === 0 || config2.brainId.trim().length === 0 || config2.principalId.trim().length === 0) {
2197
- return {
2198
- id: "auth",
2199
- status: "failed",
2200
- label: "Auth",
2201
- detail: "no authenticated CLI profile is loaded.",
2202
- fix: "Run sift login."
2461
+ function parseCliRoamMode(value) {
2462
+ if (value === "personal" || value === "workspace") return value;
2463
+ throw new Error("Roam import mode must be personal or workspace.");
2464
+ }
2465
+ function visibilityOption(parsed) {
2466
+ const visibility = optionalOption(parsed, "visibility");
2467
+ if (visibility === void 0) return void 0;
2468
+ const values = visibility.split(",").map((value) => value.trim()).filter((value) => value.length > 0);
2469
+ if (values.length === 0) {
2470
+ throw new Error("Option --visibility must include at least one visibility segment.");
2471
+ }
2472
+ return values;
2473
+ }
2474
+ function renderRoamImportResult(result) {
2475
+ return [
2476
+ `Imported ${result.importedCount} Roam pages.`,
2477
+ `Stored: ${result.storedCount}`,
2478
+ `Deduped: ${result.dedupedCount}`,
2479
+ `Rejected: ${result.rejectedCount}`,
2480
+ ""
2481
+ ].join("\n");
2482
+ }
2483
+ function roamImportUrl(apiBaseUrl, workspaceId) {
2484
+ const base = `${apiBaseUrl.replace(/\/+$/u, "")}/integrations/roam/import`;
2485
+ return workspaceId === void 0 ? base : `${base}?workspaceId=${encodeURIComponent(workspaceId)}`;
2486
+ }
2487
+ function parseJson(body) {
2488
+ try {
2489
+ return JSON.parse(body);
2490
+ } catch {
2491
+ throw new Error("Sift Roam import API returned invalid JSON.");
2492
+ }
2493
+ }
2494
+ function responseError(parsed, status2) {
2495
+ if (typeof parsed === "object" && parsed !== null) {
2496
+ const record = parsed;
2497
+ const message = record.message;
2498
+ if (typeof message === "string" && message.trim().length > 0) return message;
2499
+ const error = record.error;
2500
+ if (typeof error === "object" && error !== null && "message" in error) {
2501
+ const nested = error.message;
2502
+ if (typeof nested === "string" && nested.trim().length > 0) return nested;
2503
+ }
2504
+ }
2505
+ return `Sift Roam import API failed with status ${status2}.`;
2506
+ }
2507
+ function parseRoamImportResult(value) {
2508
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2509
+ throw new Error("Sift Roam import API returned an invalid result.");
2510
+ }
2511
+ const record = value;
2512
+ if (record.providerKind !== "roam") {
2513
+ throw new Error("Sift Roam import API returned an unexpected provider kind.");
2514
+ }
2515
+ return {
2516
+ providerKind: "roam",
2517
+ importedCount: integerField(record, "importedCount"),
2518
+ storedCount: integerField(record, "storedCount"),
2519
+ dedupedCount: integerField(record, "dedupedCount"),
2520
+ rejectedCount: integerField(record, "rejectedCount")
2521
+ };
2522
+ }
2523
+ function integerField(record, key) {
2524
+ const value = record[key];
2525
+ if (typeof value !== "number" || !Number.isInteger(value)) {
2526
+ throw new Error(`Sift Roam import API result field ${key} must be an integer.`);
2527
+ }
2528
+ return value;
2529
+ }
2530
+
2531
+ // src/mcpServeCommand.ts
2532
+ async function mcpServe(input) {
2533
+ if (input.mcpServer === void 0) {
2534
+ return fail("No local MCP server is configured for mcp.serve.");
2535
+ }
2536
+ if (input.executor === void 0) {
2537
+ return fail("Not signed in. Run 'sift login', then 'sift mcp serve'.");
2538
+ }
2539
+ const result = await input.mcpServer.serve({
2540
+ config: input.config,
2541
+ executor: input.executor,
2542
+ transport: "local_mcp"
2543
+ });
2544
+ if (result === void 0) return ok("");
2545
+ return ok(`${JSON.stringify(result)}
2546
+ `);
2547
+ }
2548
+
2549
+ // src/scopeCurrentCommand.ts
2550
+ function scopeCurrent(config, json) {
2551
+ const scope = {
2552
+ apiBaseUrl: config.apiBaseUrl,
2553
+ tokenLabel: config.tokenLabel,
2554
+ tokenExpiresAt: config.tokenExpiresAt,
2555
+ principalId: config.principalId,
2556
+ workspaceId: config.workspaceId,
2557
+ brainId: config.brainId,
2558
+ capabilities: config.capabilities
2559
+ };
2560
+ return ok(json ? `${JSON.stringify(scope)}
2561
+ ` : renderScope(scope));
2562
+ }
2563
+
2564
+ // src/specialCommands.ts
2565
+ import { readFile } from "fs/promises";
2566
+
2567
+ // src/doctor.ts
2568
+ async function doctor(input) {
2569
+ const checks = [
2570
+ { id: "bin", status: "ok", label: "Bin", detail: "sift command entrypoint is configured." },
2571
+ { id: "package", status: "ok", label: "Package", detail: "@sift-wiki/cli version unknown, bin sift." },
2572
+ authCheck(input.config, input.now),
2573
+ scopeCheck(input.config)
2574
+ ];
2575
+ const apiCheck = await checkApi(input.executor);
2576
+ checks.push(apiCheck);
2577
+ const availableTools3 = await discoverToolNames(input.executor);
2578
+ checks.push(readToolsCheck(availableTools3));
2579
+ checks.push(writeToolsCheck(input.config, availableTools3));
2580
+ checks.push(recordGetCheck(availableTools3));
2581
+ const result = {
2582
+ ok: !checks.some((check) => check.status === "failed"),
2583
+ apiBaseUrl: input.config.apiBaseUrl.trim().length > 0 ? input.config.apiBaseUrl : void 0,
2584
+ scope: scopeResult(input.config),
2585
+ checks
2586
+ };
2587
+ return {
2588
+ exitCode: result.ok ? 0 : 1,
2589
+ stdout: input.json ? `${JSON.stringify(result)}
2590
+ ` : renderDoctorResult(result),
2591
+ stderr: ""
2592
+ };
2593
+ }
2594
+ async function discoverToolNames(executor) {
2595
+ if (executor === void 0) return void 0;
2596
+ if (executor.listAvailableToolNames !== void 0) return executor.listAvailableToolNames();
2597
+ const result = await executor.execute("tools.list", {});
2598
+ return toolNamesFromResult(result);
2599
+ }
2600
+ async function apiReachability(executor) {
2601
+ if (executor === void 0) return { reachable: false, detail: "No API executor is configured." };
2602
+ try {
2603
+ await executor.execute("whoami", {});
2604
+ return { reachable: true, detail: "whoami succeeded." };
2605
+ } catch (error) {
2606
+ return {
2607
+ reachable: false,
2608
+ detail: error instanceof Error ? error.message : "Hosted Sift API request failed."
2609
+ };
2610
+ }
2611
+ }
2612
+ function scopeResult(config) {
2613
+ if (config.workspaceId.trim().length === 0 || config.brainId.trim().length === 0 || config.principalId.trim().length === 0) {
2614
+ return void 0;
2615
+ }
2616
+ return {
2617
+ workspaceId: config.workspaceId,
2618
+ brainId: config.brainId,
2619
+ principalId: config.principalId,
2620
+ capabilities: [...config.capabilities]
2621
+ };
2622
+ }
2623
+ async function checkApi(executor) {
2624
+ if (executor === void 0) {
2625
+ return {
2626
+ id: "api",
2627
+ status: "warning",
2628
+ label: "API",
2629
+ detail: "not checked because no API executor is configured.",
2630
+ fix: "Run sift login or configure hosted API credentials."
2631
+ };
2632
+ }
2633
+ const api = await apiReachability(executor);
2634
+ return api.reachable ? { id: "api", status: "ok", label: "API", detail: "whoami succeeded." } : { id: "api", status: "failed", label: "API", detail: api.detail };
2635
+ }
2636
+ function authCheck(config, now) {
2637
+ if (config.apiBaseUrl.trim().length === 0 || config.workspaceId.trim().length === 0 || config.brainId.trim().length === 0 || config.principalId.trim().length === 0) {
2638
+ return {
2639
+ id: "auth",
2640
+ status: "failed",
2641
+ label: "Auth",
2642
+ detail: "no authenticated CLI profile is loaded.",
2643
+ fix: "Run sift login."
2203
2644
  };
2204
2645
  }
2205
- if (config2.tokenExpiresAt !== void 0 && Date.parse(config2.tokenExpiresAt) <= now.getTime()) {
2646
+ if (config.tokenExpiresAt !== void 0 && Date.parse(config.tokenExpiresAt) <= now.getTime()) {
2206
2647
  return {
2207
2648
  id: "auth",
2208
2649
  status: "failed",
@@ -2213,8 +2654,8 @@ function authCheck(config2, now) {
2213
2654
  }
2214
2655
  return { id: "auth", status: "ok", label: "Auth", detail: "authenticated profile loaded." };
2215
2656
  }
2216
- function scopeCheck(config2) {
2217
- if (config2.workspaceId.trim().length === 0 || config2.brainId.trim().length === 0) {
2657
+ function scopeCheck(config) {
2658
+ if (config.workspaceId.trim().length === 0 || config.brainId.trim().length === 0) {
2218
2659
  return {
2219
2660
  id: "scope",
2220
2661
  status: "failed",
@@ -2227,14 +2668,14 @@ function scopeCheck(config2) {
2227
2668
  id: "scope",
2228
2669
  status: "ok",
2229
2670
  label: "Scope",
2230
- detail: `${config2.workspaceId}/${config2.brainId}`
2671
+ detail: `${config.workspaceId}/${config.brainId}`
2231
2672
  };
2232
2673
  }
2233
2674
  function readToolsCheck(names) {
2234
2675
  return toolSetCheck("read-tools", "Read tools", ["context.assemble", "search.query"], names);
2235
2676
  }
2236
- function writeToolsCheck(config2, names) {
2237
- const hasWrite = config2.capabilities.includes("record:write") || config2.capabilities.includes("source:write");
2677
+ function writeToolsCheck(config, names) {
2678
+ const hasWrite = config.capabilities.includes("record:write") || config.capabilities.includes("source:write");
2238
2679
  if (!hasWrite) {
2239
2680
  return {
2240
2681
  id: "write-tools",
@@ -2273,9 +2714,9 @@ function toolSetCheck(id, label, required, names) {
2273
2714
  const missing = required.filter((name) => !names.includes(name));
2274
2715
  return missing.length === 0 ? { id, status: "ok", label, detail: "required tools are available." } : { id, status: "failed", label, detail: `missing ${missing.join(", ")}.` };
2275
2716
  }
2276
- function toolNamesFromResult(result2) {
2277
- if (!Array.isArray(result2)) return [];
2278
- return result2.flatMap((item) => {
2717
+ function toolNamesFromResult(result) {
2718
+ if (!Array.isArray(result)) return [];
2719
+ return result.flatMap((item) => {
2279
2720
  if (typeof item === "object" && item !== null && "name" in item) {
2280
2721
  const name = item.name;
2281
2722
  return typeof name === "string" ? [name] : [];
@@ -2287,6 +2728,7 @@ function toolNamesFromResult(result2) {
2287
2728
  // src/simpleCommands.ts
2288
2729
  var knownTopLevelCommands = /* @__PURE__ */ new Set([
2289
2730
  "add",
2731
+ "agent",
2290
2732
  "ask",
2291
2733
  "audit",
2292
2734
  "auth",
@@ -2307,6 +2749,7 @@ var knownTopLevelCommands = /* @__PURE__ */ new Set([
2307
2749
  "mcp",
2308
2750
  "record",
2309
2751
  "remember",
2752
+ "roam",
2310
2753
  "scope",
2311
2754
  "search",
2312
2755
  "show",
@@ -2399,11 +2842,11 @@ async function ask(executor, rest, json) {
2399
2842
  }
2400
2843
  const parsed = parseOptions(rest);
2401
2844
  const query = argsWithoutOptions(rest).join(" ").trim();
2402
- const result2 = await executor.execute("context.assemble", {
2845
+ const result = await executor.execute("context.assemble", {
2403
2846
  query,
2404
2847
  maxChars: parseIntegerOption(parsed, "max-chars", 8e3)
2405
2848
  });
2406
- return ok(json ? aliasJson("ask", "context.assemble", result2) : renderSiftFound(result2));
2849
+ return ok(json ? aliasJson("ask", "context.assemble", result) : renderSiftFound(result));
2407
2850
  }
2408
2851
  async function simpleSearch(executor, rest, json) {
2409
2852
  if (executor === void 0) {
@@ -2411,11 +2854,11 @@ async function simpleSearch(executor, rest, json) {
2411
2854
  }
2412
2855
  const parsed = parseOptions(rest);
2413
2856
  const query = argsWithoutOptions(rest).join(" ").trim();
2414
- const result2 = await executor.execute("search.query", {
2857
+ const result = await executor.execute("search.query", {
2415
2858
  query,
2416
2859
  limit: parseIntegerOption(parsed, "limit", 10)
2417
2860
  });
2418
- return ok(json ? aliasJson("search", "search.query", result2) : renderSearchResult(result2));
2861
+ return ok(json ? aliasJson("search", "search.query", result) : renderSearchResult(result));
2419
2862
  }
2420
2863
  async function remember(executor, rest, json, readStdin2, now) {
2421
2864
  if (executor === void 0) {
@@ -2434,8 +2877,8 @@ async function remember(executor, rest, json, readStdin2, now) {
2434
2877
  visibility: [optionalOption(parsed, "visibility") ?? DEFAULT_CLI_VISIBILITY],
2435
2878
  markdown
2436
2879
  };
2437
- const result2 = await executor.execute("capture.text", input);
2438
- return ok(json ? aliasJson("remember", "capture.text", result2) : renderWriteReceipt("Remember", result2));
2880
+ const result = await executor.execute("capture.text", input);
2881
+ return ok(json ? aliasJson("remember", "capture.text", result) : renderWriteReceipt("Remember", result));
2439
2882
  }
2440
2883
  async function addFile(executor, fileReader, rest, json) {
2441
2884
  if (executor === void 0) {
@@ -2457,8 +2900,8 @@ async function addFile(executor, fileReader, rest, json) {
2457
2900
  bytes,
2458
2901
  visibility: [optionalOption(parsed, "visibility") ?? DEFAULT_CLI_VISIBILITY]
2459
2902
  };
2460
- const result2 = await executor.execute("capture.file", input);
2461
- return ok(json ? aliasJson("add", "capture.file", result2) : renderWriteReceipt("Add", result2));
2903
+ const result = await executor.execute("capture.file", input);
2904
+ return ok(json ? aliasJson("add", "capture.file", result) : renderWriteReceipt("Add", result));
2462
2905
  }
2463
2906
  async function edit(executor, rest, json) {
2464
2907
  if (executor === void 0) {
@@ -2483,8 +2926,8 @@ async function edit(executor, rest, json) {
2483
2926
  if (expectedMarkdown !== void 0) {
2484
2927
  input.expectedMarkdown = expectedMarkdown;
2485
2928
  }
2486
- const result2 = await executor.execute("record.patch_section", input);
2487
- return ok(json ? aliasJson("edit", "record.patch_section", result2) : renderWriteReceipt("Edit", result2));
2929
+ const result = await executor.execute("record.patch_section", input);
2930
+ return ok(json ? aliasJson("edit", "record.patch_section", result) : renderWriteReceipt("Edit", result));
2488
2931
  }
2489
2932
  async function decide(executor, rest, json) {
2490
2933
  if (executor === void 0) {
@@ -2497,8 +2940,8 @@ async function decide(executor, rest, json) {
2497
2940
  visibility: [optionalOption(parsed, "visibility") ?? DEFAULT_CLI_VISIBILITY]
2498
2941
  };
2499
2942
  addOptionalWorkAliasMetadata(input, parsed);
2500
- const result2 = await executor.execute("decision.create", input);
2501
- return ok(json ? aliasJson("decide", "decision.create", result2) : renderWriteReceipt("Decision", result2));
2943
+ const result = await executor.execute("decision.create", input);
2944
+ return ok(json ? aliasJson("decide", "decision.create", result) : renderWriteReceipt("Decision", result));
2502
2945
  }
2503
2946
  async function todo(executor, rest, json) {
2504
2947
  if (executor === void 0) {
@@ -2515,8 +2958,8 @@ async function todo(executor, rest, json) {
2515
2958
  const dueDate = optionalOption(parsed, "due-date");
2516
2959
  if (dueDate !== void 0) input.dueDate = dueDate;
2517
2960
  addOptionalWorkAliasMetadata(input, parsed);
2518
- const result2 = await executor.execute("task.create", input);
2519
- return ok(json ? aliasJson("todo", "task.create", result2) : renderWriteReceipt("Task", result2));
2961
+ const result = await executor.execute("task.create", input);
2962
+ return ok(json ? aliasJson("todo", "task.create", result) : renderWriteReceipt("Task", result));
2520
2963
  }
2521
2964
  async function show(executor, rest, json) {
2522
2965
  if (executor === void 0) {
@@ -2539,21 +2982,21 @@ async function show(executor, rest, json) {
2539
2982
  if (sectionAnchor !== void 0) {
2540
2983
  input.sectionAnchor = sectionAnchor;
2541
2984
  }
2542
- const result2 = await executor.execute("record.get", input);
2543
- return ok(json ? aliasJson("show", "record.get", result2) : renderRecordResult(result2));
2985
+ const result = await executor.execute("record.get", input);
2986
+ return ok(json ? aliasJson("show", "record.get", result) : renderRecordResult(result));
2544
2987
  }
2545
- async function status(config2, executor, json) {
2988
+ async function status(config, executor, json) {
2546
2989
  const scope = {
2547
- apiBaseUrl: config2.apiBaseUrl,
2548
- principalId: config2.principalId,
2549
- workspaceId: config2.workspaceId,
2550
- brainId: config2.brainId,
2551
- capabilities: [...config2.capabilities]
2990
+ apiBaseUrl: config.apiBaseUrl,
2991
+ principalId: config.principalId,
2992
+ workspaceId: config.workspaceId,
2993
+ brainId: config.brainId,
2994
+ capabilities: [...config.capabilities]
2552
2995
  };
2553
2996
  const api = await apiReachability(executor);
2554
- const result2 = { scope, api };
2997
+ const result = { scope, api };
2555
2998
  if (json) {
2556
- return ok(`${JSON.stringify({ command: "status", result: result2 })}
2999
+ return ok(`${JSON.stringify({ command: "status", result })}
2557
3000
  `);
2558
3001
  }
2559
3002
  return ok(`${renderScope({ ...scope, tokenLabel: "configured" })}API reachable: ${api.reachable}
@@ -2596,6 +3039,201 @@ function argsWithoutOptions(args) {
2596
3039
  return positionals;
2597
3040
  }
2598
3041
 
3042
+ // src/skill/skillCommands.ts
3043
+ import { access, mkdir as nodeMkdir, writeFile as nodeWriteFile } from "fs/promises";
3044
+ import { homedir } from "os";
3045
+ import { isAbsolute, join, resolve } from "path";
3046
+
3047
+ // src/skill/skillContent.ts
3048
+ var SIFT_SETUP_SKILL_MARKDOWN = '---\nname: sift-setup\ndescription: Connect this agent to Sift, the team\'s shared cited brain, by setting up the Sift CLI: install it, have the human sign in, register this agent on the workspace, and confirm it works. Use this skill whenever the user pastes a Sift onboarding/setup prompt or says anything like "set up Sift", "connect me to Sift", "install the sift CLI", "sign me in to Sift", or "give you access to our brain" \u2014 even if they never say "CLI". This is first-run setup; once `sift doctor` is green you have full access and can use the brain. Hand off to the sift-cli skill for the full read/write playbook.\n---\n\n# Sift Setup\n\nSift is your team\'s shared, cited brain: context that people and agents both read\nand write. The `sift` CLI is how you reach it. Once the human signs in and you\nregister yourself, you are an agent on the workspace with full access \u2014 you read\nthe brain before answering and write back to it like a teammate.\n\nWork top to bottom. Each step says how to confirm it. If a step is genuinely\nblocked, say so and ask the human for the one thing you need \u2014 do not fake\nprogress. Never print token values, `.env` contents, or keychain secrets.\n\n## 1. Install the CLI\n\nThe package is `@sift-wiki/cli` (Node.js 20+); it installs a `sift` command.\n\n- If it will be used repeatedly, install it globally:\n\n ```bash\n npm install -g @sift-wiki/cli\n ```\n\n- For a one-off or a sandbox, run it with no install: `npx -y\n @sift-wiki/cli@latest <command>`.\n\nIf `sift` is missing right after a global install, npm\'s global bin directory is\nnot on `PATH` \u2014 find it with `npm config get prefix` and ensure `<prefix>/bin` is\non `PATH`. If the install fails, confirm Node is 20+ with `node --version`.\n\nConfirm: `sift auth status --json` runs (it will say `{"auth":"none"}` until\nsign-in \u2014 expected).\n\n## 2. The human signs in\n\n`sift login` opens the browser sign-in and stores the profile on this machine.\n\n- Run `sift login` (or ask the human to), then have them finish the approval in\n the browser tab that opened. Wait for it to complete. One sign-in covers this\n machine.\n- Headless box with no browser (CI, a remote runner): use env-token auth instead\n with credentials the human provides \u2014 `SIFT_API_BASE_URL`, `SIFT_API_TOKEN`,\n `SIFT_WORKSPACE_ID`, `SIFT_BRAIN_ID`, `SIFT_PRINCIPAL_ID`,\n `SIFT_TOKEN_CAPABILITIES`. Never invent these.\n\nConfirm: `sift auth status --json` reports `"auth":"stored"` (sign-in) or\n`"auth":"env"` (token).\n\n## 3. Register yourself as an agent\n\nThis puts you on the workspace as an agent and signs your writes as you.\n\n- Set `SIFT_AGENT="<your product name>"` on every `sift` call (for example\n `SIFT_AGENT="Claude Code"`), or pass `--as-agent "<name>"`. Use your own\n product identity \u2014 never the human\'s name, never the device label.\n- Register once:\n\n ```bash\n sift agent register --name "<your product name>" --description "<one line>"\n ```\n\n Registration is idempotent and makes you visible to the workspace on the People\n page. If your name is refused as "removed from this workspace," a human removed\n it deliberately \u2014 stop and tell the user; do not assert a different name.\n\nConfirm: `sift agent status --json` shows your registration.\n\n## 4. Confirm it works\n\n```bash\nsift doctor --json\n```\n\nWhen the checks pass, you are connected with full access \u2014 say so. If a check\nfails, fix that one thing (missing command \u2192 install; missing auth \u2192 `sift\nlogin`; API unreachable \u2192 report the configured base URL and ask the human).\nDon\'t fall back to local files; local files are not the brain.\n\n## 5. Now use it \u2014 write like a teammate\n\nYou have full access. Treat Sift as your source of truth.\n\n- Read before answering: `sift ask "<question>"` or `sift search "<query>"`.\n- Before substantive work, run `sift context assemble "<task>"` or the equivalent\n `sift ask` path and inspect any returned `## Task guidance` section. That\n section may contain relevant workspace skills/custom prompts. Follow it before\n producing output, and if you use a matched skill and your token can write,\n record `skill.exercise` after the output.\n- **Write freely** for routine, non-destructive things \u2014 capturing notes,\n context, decisions, and tasks (`sift remember`, `sift add`, `sift decide`,\n `sift todo`). You don\'t need to ask permission to record what\'s worth keeping;\n that\'s the job.\n- **Ask the human first only for important or destructive changes** \u2014 deleting or\n overwriting existing records, editing someone else\'s work, or recording a\n consequential decision. The test is simple: if it\'s hard to undo or could\n mislead the team, confirm first; otherwise just do it.\n\nFor the full read/write playbook (context assembly, capture-before-derived,\npatching records, citations), load the companion skill:\n\n```bash\nsift skill print sift-cli\n```\n\n## Report back\n\nWhen setup is done, tell the human, without exposing secrets: which CLI path you\nused, the auth source (`stored`, `env`, or `none`), the agent name you\nregistered, and that the brain is reachable \u2014 or the one step that\'s blocked and\nwhat you need from them.\n';
3049
+ var SIFT_CLI_SKILL_MARKDOWN = '---\nname: sift-cli\ndescription: Use this skill whenever an agent needs to use the Sift CLI to read from or write to the Sift brain, including searching, assembling context, capturing text or files, patching records, creating decisions or tasks, debugging auth/scope, handling local API or sandbox failures, or falling back from missing `sift` on PATH. Prefer this skill for Sift CLI work even if the user only says "use Sift", "query the brain", "capture this", "remember this", "record a decision", or "what is latest in Sift".\n---\n\n# Sift CLI\n\nUse the Sift CLI as a thin client to the hosted Sift brain. The hosted brain is\ncanonical. Local files, terminal output, and chat text are not canonical until\ncaptured into Sift.\n\nThe CLI package is `@sift-wiki/cli` and it installs a `sift` bin. The package is\nlive on npm, npm-first, and Node.js 20+. Install or upgrade with `@latest`. It is\na command package, not a public SDK. Do not import it as a library, publish\ninternal Sift packages, or make local files a source of truth.\n\nInstall the live CLI with:\n\n```bash\nnpm install -g @sift-wiki/cli\n```\n\nThe CLI bundles its own agent skills, versioned with the package. The first-run\nsetup skill is `sift-setup`; `sift skill install` installs it by default (writes\n`.claude/skills/sift-setup/SKILL.md`; add `--global` for `~/.claude/skills` or\n`--dir <path>`). Install this usage skill on disk with\n`sift skill install sift-cli`, print any skill with `sift skill print <name>`,\nor list bundled skills with `sift skill list`. The zero-install setup entry point\nis `npx -y @sift-wiki/cli@latest skill install`, which works before any global\ninstall. These skill commands are local and need no auth.\n\nFor one-off or headless use without a global install, run the live package\ndirectly from npm:\n\n```bash\nnpx -y @sift-wiki/cli@latest auth status --json\nnpm exec --yes --package @sift-wiki/cli@latest -- sift auth status --json\n```\n\nFor CLI distribution changes inside the repo, build, pack, and verify the\ninstalled tarball with `pnpm --filter @sift-wiki/cli pack:verify` before a\nrelease. This private monorepo owns the CLI source and verifier; npm publishes\nare cut from the public `goodnight000/sift-cli` release mirror so npm provenance\ncan point at public GitHub release source.\n\n## Preflight\n\nBefore reading or writing, establish the command path, auth, and scope.\n\n1. Prefer installed `sift` when it exists on `PATH`.\n2. If `sift` is missing outside the repo and the user needs repeated\n interactive use, install the live package with\n `npm install -g @sift-wiki/cli`.\n3. If `sift` is missing outside the repo and the user needs one-off, CI, or\n headless execution, use\n `npm exec --yes --package @sift-wiki/cli@latest -- sift ...` or\n `npx -y @sift-wiki/cli@latest ...`.\n4. For normal human setup, use `sift login`; it opens the existing browser login\n flow and stores the CLI profile.\n5. For CI/headless agents only, use env-token auth when the user or environment\n provides it: `SIFT_API_BASE_URL`, `SIFT_API_TOKEN`, `SIFT_WORKSPACE_ID`,\n `SIFT_BRAIN_ID`, `SIFT_PRINCIPAL_ID`, and `SIFT_TOKEN_CAPABILITIES`.\n6. Do not print `.env` files, token values, keychain output, bearer secrets, or\n full credential-store output.\n7. Check auth and scope with `sift auth status --json`, then\n `sift scope current --json` when authenticated.\n8. Declare your agent identity on every invocation: set\n `SIFT_AGENT="<your product name>"` (for example `SIFT_AGENT="Claude Code"`)\n in the environment of each `sift` call, or pass `--as-agent "<name>"`. Use\n your own product identity \u2014 never the device label and never the human\'s\n name. This keeps authorship correct when several agents share one CLI\n profile and token; your writes are stamped as you, acting for the human who\n approved the token. A human running `sift` directly sets nothing and stays\n plainly themselves.\n9. Once authenticated, check `sift agent status --json`; if it reports no\n agent identity registration for your name, run `sift agent register` with\n your product name and a one-line description. Registration is idempotent\n (re-running converges on the same identity and refreshes the description),\n requires only your usable token, and makes you visible to the workspace on\n the People page. First use of a `SIFT_AGENT` name also auto-registers it;\n explicit register is how you add a self-description.\n10. Run `sift doctor --json` when setup, auth, API reachability, or tool\n availability is unclear.\n11. In the `sift-v3` repo only, if `sift` is missing and you need the\n development CLI, build first and run the built JS entrypoint:\n\n ```bash\n pnpm --filter @sift-wiki/cli build\n node packages/cli/dist/bin/sift.js auth status --json\n ```\n\n12. Do not run TypeScript source as the CLI bin. The package bin points to\n `dist/bin/sift.js`.\n13. If a local development API is required and down, report that directly. Do\n not silently switch to local files.\n\nUse package verification only when the task is about distribution or installed\nartifact proof:\n\n```bash\npnpm --filter @sift-wiki/cli pack:verify\n```\n\nThat verifier packs `@sift-wiki/cli`, installs the tarball into an isolated npm\nprefix, runs installed unauthenticated `sift auth status --json`, then uses\nenv-token auth against a local fake hosted API to prove `auth status`,\n`whoami --json`, and `ask "package smoke" --json` with bearer, workspace, and\nbrain headers.\n\nStop and ask for the minimum missing permission or setup action when:\n\n- no runnable CLI path exists;\n- auth is missing or expired;\n- workspace or brain scope is missing;\n- the hosted/local API cannot be reached from the runtime;\n- sandbox/network restrictions block the command;\n- a required write is requested with read-only capabilities;\n- tool discovery says the required runtime contract is unavailable.\n- the user asks to publish a new CLI version that has not passed post-publish\n install smoke.\n\n## Fast Read Path\n\nUse one focused context command before broad search loops.\n\nPreferred simple commands, when available:\n\n```bash\nsift "what is latest with the company?"\nsift ask "what changed since the last meeting?"\nsift search "Slack ingestion launch"\nsift status --json\nsift whoami --json\n```\n\nCurrent power-command fallback:\n\n```bash\nsift context assemble "what is latest with the company?" --max-chars 12000 --json\nsift search query "Slack ingestion launch" --json\nsift context profile "reviewer evidence" --limit 6 --json\n```\n\nRules:\n\n- Use `context assemble` for grounded answers, summaries, handoffs, write\n preparation, and "latest/current" questions.\n- Before substantive work, inspect any returned `## Task guidance` section. It\n may contain relevant workspace skills/custom prompts; follow the matched\n skill/custom prompt before producing output.\n- If `## Task guidance` names a matched skill and you produce output informed by\n it, call `skill exercise` / `skill.exercise` after the output when your token\n can write. Use the skill id, pinned version id, surface, outputRef, and a\n stable idempotency key. If the token is read-only, do not claim exercise\n attribution was recorded.\n- Use `search query` for raw retrieval or to find candidate records.\n- Do not call `record get` until `tools list` or `doctor` proves it is backed by\n the runtime; older slices may advertise it while returning `tool_unavailable`.\n- Keep broad context calls capped with `--max-chars`; increase only when the\n returned context is too thin.\n- Include Sift citations, record IDs, version IDs, source IDs, headings, or\n chunk locators when the CLI returns them.\n\n## Fast Write Path\n\nWrites must go through Sift tools, not local notes.\n\nPreferred simple commands, when available:\n\n```bash\nsift remember "Follow up with Caleb about the Underscore intro."\nsift remember --stdin\nsift add ./meeting-notes.md\nsift decide "Ship the retrieval-only slice first."\nsift todo "Collect three evidence examples for onboarding."\nsift edit <record-id> --section Risks --replace "..."\n```\n\nCurrent power-command fallbacks:\n\n```bash\nsift capture text \\\n --source "CLI Capture" \\\n --external-id "cli-text:<stable-id>" \\\n --title "Follow up with Caleb" \\\n --visibility team \\\n --markdown "Follow up with Caleb about the Underscore intro."\n\nsift capture file ./meeting-notes.md \\\n --source "CLI Capture" \\\n --external-id "cli-file:<stable-id>" \\\n --title "Meeting notes" \\\n --visibility team\n\nsift decision create \\\n --statement "Ship the retrieval-only slice first." \\\n --state accepted \\\n --visibility team\n\nsift task create \\\n --title "Collect three evidence examples for onboarding." \\\n --status open \\\n --visibility team\n\nsift record patch-section <record-id> risks \\\n --replacement-markdown "..." \\\n --expected-markdown "..."\n```\n\nRules:\n\n- Verify the token has `record:write` before writes.\n- Verify the token has `source:write` before `remember`, `add`, `capture text`,\n `capture file`, or `capture batch`.\n- Prefer capture before derived records when the user supplies raw source.\n- Use stable external IDs for capture retries.\n- Read a record before patching it, and include expected content when available.\n- If a conflict is returned, surface current version metadata and do not\n overwrite.\n- Print or summarize write receipts with record, version, source item, and job\n IDs. Do not claim a write happened without a receipt.\n\n## Troubleshooting\n\nClassify failures before retrying.\n\n- **Command missing:** for repeated interactive use, install the live package\n with `npm install -g @sift-wiki/cli`. For one-off or headless use, run\n `npm exec --yes --package @sift-wiki/cli@latest -- sift ...` or\n `npx -y @sift-wiki/cli@latest ...`. In `sift-v3`, build and use\n `node packages/cli/dist/bin/sift.js` as the development fallback when working\n on unpublished local CLI changes.\n- **Auth missing:** run or request `sift login`; use env-token auth only when it\n is explicitly provided for CI/headless use.\n- **Read-only token:** reads may proceed, writes must stop.\n- **API down:** report the configured API base URL and failure class.\n- **Sandbox network block:** rerun with approved escalation only when the user\n asked for live CLI execution and policy allows it.\n- **Local listener blocked:** `pack:verify` uses a fake API on `127.0.0.1`; if\n the sandbox returns `listen EPERM`, rerun with approved escalation instead of\n weakening the installed-artifact test.\n- **Tool unavailable:** use `tools list`, `tools help`, or `doctor`; do not\n retry the same unsupported command repeatedly.\n- **Agent identity refused:** a `SIFT_AGENT` name that returns "has been\n removed from this workspace" was deliberately removed by a human. Stop and\n tell the user; do not work around it by asserting a different name.\n- **`agent` commands unavailable:** the installed CLI or the API predates\n agent workers; proceed without identity assertion and note the limitation.\n- **GitHub Actions publish blocked:** package publishing runs from the public\n `goodnight000/sift-cli` release mirror. The private `sift-v3` workflow is\n verify-only; do not publish `@sift-wiki/cli` from the private repo.\n- **Large output:** reduce `--max-chars`, search more narrowly, then assemble\n context for the narrowed query.\n\n## Expected Response\n\nWhen answering the user after CLI work:\n\n- State which CLI path was used: installed `sift`, zero-install `npx`/`npm exec`,\n built repo JS, or verified packed tarball.\n- State the auth source only at a safe level: `stored`, `env`, or `none`.\n- State the agent identity asserted (the `SIFT_AGENT` name), or that none was\n set.\n- State the API scope used without exposing secrets.\n- Summarize the answer or write result in normal prose.\n- Include Sift citations or write receipt IDs.\n- Call out limitations such as read-only auth, local API dependency, missing\n runtime contract, sandbox escalation, unpublished package version, or\n stale/unverified context.\n';
3050
+ var SIFT_AGENT_SKILL_MARKDOWN = '---\nname: sift-agent\ndescription: Use this skill whenever an external coding or research agent needs to read from, capture into, patch, or create work objects in a Sift brain through Sift CLI or MCP tools. Prefer this skill when the user mentions Sift, the hosted brain, brain records, captured source, cited context, decisions, tasks, or choosing between CLI and MCP access.\n---\n\n# Sift Agent\n\nUse Sift as the canonical shared brain for accumulated human and agent context.\nThe hosted Sift brain is canonical. Local files are not canonical brain state\nunless they have been captured into Sift; local files are not canonical by\nthemselves.\n\n## The Contract Comes First\n\nBefore any other Sift work, fetch the Sift agent contract and read it:\n\n- CLI: run `sift contract get --json`.\n- MCP: call `contract.get`.\n\nThe contract is the authoritative protocol (reading, writing, the learning\nloop, restraint) plus this workspace\'s own rules. Echo the returned\n`contractVersion` in the input of every other tool call (CLI: pass\n`--contract <version>`). A call refused with `contract_required` returns the\ncurrent contract in the error message \u2014 read it and retry with the new\nversion. The full rules live in the served contract, not in this file.\n\n## Transport Choice\n\nAgents should prefer CLI when it is already installed, authenticated, and\nscoped; otherwise use MCP.\n\n1. Prefer CLI when `sift` is installed, authenticated, and already scoped to the\n correct workspace and brain.\n2. Use MCP when CLI is unavailable, unauthenticated, unscoped, or blocked by the\n runtime.\n3. Do not ask the user to install CLI during the task if MCP tools are already\n available.\n4. Treat both CLI and MCP as access transports. Do not treat local markdown,\n local manifests, or chat transcript text as canonical storage.\n\n## First Checks\n\nAfter fetching the contract, establish identity and scope:\n\n- CLI: run `sift scope current --json` or `sift whoami --json`.\n- MCP: call `scope.current` or `whoami`.\n\nIf the tool reports missing scope, expired auth, or insufficient capability,\nreturn the compact tool error and ask for the minimum permission or reconnect\naction needed. Do not guess a workspace, brain, source, or principal.\n\n## Agent Identity\n\nDeclare who you are so your writes are authored as you, acting for the human\nwho approved the token:\n\n1. Set `SIFT_AGENT="<your product name>"` (for example "Claude Code") in the\n environment of every CLI invocation, or pass `--as-agent "<name>"`. Use\n your own product identity \u2014 never the device label and never the human\'s\n name. First use auto-registers you as a workspace agent worker, visible on\n the People page.\n2. Check `sift agent status --json` (CLI) or `agent.status` (MCP); register a\n one-line self-description with `sift agent register --name "<name>"\n --description "<one line>"` or `agent.register`. Registration is idempotent\n and requires only your usable token.\n3. Identity is authorship, not authority: asserting a name never changes what\n the token may read or write.\n4. If your asserted name is refused as removed from the workspace, stop and\n tell the user; do not assert a different name to work around it.\n\n## Search And Context\n\nSearch and assemble context before answering from memory:\n\n1. Use `search.query` for targeted lookup.\n2. Use `context.assemble` when the user needs a grounded answer, handoff, patch,\n decision, or task.\n3. Before substantive work, inspect any returned `## Task guidance` section.\n It may contain the relevant workspace skills/custom prompts for this task;\n follow the matched skill/custom prompt before producing output.\n4. Use `context.profile` only for durable profile or workspace context.\n5. Keep responses grounded in returned Sift citations. Do not invent facts\n outside the brain.\n\nCite record IDs, version IDs, source IDs, source item IDs, heading anchors, and\nchunk locators when the tool returns them. Prefer compact cited summaries over\ndumping large content.\n\nIf `## Task guidance` names a matched skill and you produce output informed by\nit, call `skill.exercise` after the output when your token can write. Use the\nskill id, pinned version id, surface (`cli`, `mcp`, or `api`), an outputRef, and\na stable idempotency key. If your token is read-only, do not claim exercise\nattribution was recorded.\n\n## Capture And Derived Writes\n\nCapture raw source before creating derived knowledge when possible:\n\n1. Use `capture.text` for copied text or markdown.\n2. Use `capture.file` for local files; the file path is only an input, not\n canonical brain state.\n3. Use `capture.batch` for bounded repeatable imports.\n4. Reuse stable external IDs or idempotency keys when retrying capture.\n\nOnly create a derived markdown record after the relevant raw source is captured\nor after the user explicitly asks for an authored record without source capture.\n\n## Records And Patches\n\nRead the current record version before editing. Patch bounded sections instead\nof rewriting whole records:\n\n- Use `record.get` to inspect the current record or requested section.\n- Patch bounded sections with `record.patch_section`.\n- Include expected content when available so conflicts are detected.\n- If a patch conflict is returned, show the current version metadata and ask for\n the next edit boundary. Do not silently overwrite.\n\n## Decisions And Tasks\n\nCreate decisions and tasks only from explicit user intent or grounded context:\n\n- Use `decision.create` when the user asks to record a decision or the context\n clearly contains a chosen course of action.\n- Use `task.create` when the user asks to track follow-up work or the context\n clearly assigns a next action.\n- Include rationale or explicit authorship.\n- Link evidence when available.\n- Do not create thread-like records; `thread.create` is unavailable until the\n Collaboration Threads spec owns that behavior.\n\n## Safety Boundaries\n\n- Do not use destructive forget, delete, broad admin, connector OAuth install,\n or hosted-agent run-control tools in this first tool slice.\n- Do not expose token values, secrets, raw private snippets, or full provider\n payloads in logs or user-facing messages.\n- Preserve principal, actor, request, workspace, brain, and source scope across\n tool calls.\n- If a permission denial hides a private object, do not reveal private\n existence, title, count, or snippet.\n';
3051
+
3052
+ // src/skill/skillCommands.ts
3053
+ var BUNDLED_SKILLS = [
3054
+ {
3055
+ name: "sift-setup",
3056
+ summary: "Set up the Sift CLI and connect this agent to the brain (first run).",
3057
+ markdown: SIFT_SETUP_SKILL_MARKDOWN
3058
+ },
3059
+ {
3060
+ name: "sift-cli",
3061
+ summary: "Set up and use the Sift CLI as a thin client to the hosted brain.",
3062
+ markdown: SIFT_CLI_SKILL_MARKDOWN
3063
+ },
3064
+ {
3065
+ name: "sift-agent",
3066
+ summary: "Read, capture, and patch the Sift brain over CLI or MCP.",
3067
+ markdown: SIFT_AGENT_SKILL_MARKDOWN
3068
+ }
3069
+ ];
3070
+ var DEFAULT_SKILL = "sift-setup";
3071
+ function defaultSkillIo(input) {
3072
+ return {
3073
+ writeFile: (path, data) => nodeWriteFile(path, data, "utf8"),
3074
+ mkdir: async (path) => {
3075
+ await nodeMkdir(path, { recursive: true });
3076
+ },
3077
+ pathExists: async (path) => {
3078
+ try {
3079
+ await access(path);
3080
+ return true;
3081
+ } catch {
3082
+ return false;
3083
+ }
3084
+ },
3085
+ homeDir: input?.homeDir ?? homedir(),
3086
+ cwd: input?.cwd ?? process.cwd()
3087
+ };
3088
+ }
3089
+ async function runSkillCommand(input) {
3090
+ const [subcommand, ...rest] = input.rest;
3091
+ if (subcommand === void 0 || subcommand === "list") {
3092
+ return listSkills(input.json);
3093
+ }
3094
+ if (subcommand === "print" || subcommand === "show") {
3095
+ return printSkill(rest, input.json);
3096
+ }
3097
+ if (subcommand === "install") {
3098
+ return installSkill(rest, input.json, input.io);
3099
+ }
3100
+ return errorResultWithCode(
3101
+ "tool_unavailable",
3102
+ `Unknown skill subcommand '${subcommand}'. Use 'list', 'print [name]', or 'install [name]'.`,
3103
+ input.json
3104
+ );
3105
+ }
3106
+ function listSkills(json) {
3107
+ if (json) {
3108
+ return ok(
3109
+ `${JSON.stringify({
3110
+ skills: BUNDLED_SKILLS.map(({ name, summary }) => ({ name, summary }))
3111
+ })}
3112
+ `
3113
+ );
3114
+ }
3115
+ const lines = BUNDLED_SKILLS.map((skill) => `${skill.name}: ${skill.summary}`);
3116
+ return ok(`${lines.join("\n")}
3117
+ `);
3118
+ }
3119
+ function printSkill(rest, json) {
3120
+ const requested = positionalArgs(rest)[0];
3121
+ const skill = findSkill(requested);
3122
+ if (skill === void 0) {
3123
+ return unknownSkill(requested, json);
3124
+ }
3125
+ if (json) {
3126
+ return ok(`${JSON.stringify({ skill: skill.name, markdown: skill.markdown })}
3127
+ `);
3128
+ }
3129
+ return ok(withTrailingNewline(skill.markdown));
3130
+ }
3131
+ async function installSkill(rest, json, io) {
3132
+ const requested = positionalArgs(rest)[0];
3133
+ const skill = findSkill(requested);
3134
+ if (skill === void 0) {
3135
+ return unknownSkill(requested, json);
3136
+ }
3137
+ const parsed = parseOptions(rest);
3138
+ const baseDir = resolveBaseDir({ rest, parsed, io });
3139
+ const targetDir = join(baseDir, skill.name);
3140
+ const targetPath = join(targetDir, "SKILL.md");
3141
+ const existed = await io.pathExists(targetPath);
3142
+ await io.mkdir(targetDir);
3143
+ const markdown = withTrailingNewline(skill.markdown);
3144
+ await io.writeFile(targetPath, markdown);
3145
+ const status2 = existed ? "updated" : "created";
3146
+ if (json) {
3147
+ return ok(
3148
+ `${JSON.stringify({
3149
+ installed: true,
3150
+ skill: skill.name,
3151
+ path: targetPath,
3152
+ status: status2,
3153
+ bytes: Buffer.byteLength(markdown, "utf8")
3154
+ })}
3155
+ `
3156
+ );
3157
+ }
3158
+ return ok(renderInstall(skill.name, targetPath, status2));
3159
+ }
3160
+ function resolveBaseDir(input) {
3161
+ const explicit = optionalOption(input.parsed, "dir");
3162
+ if (explicit !== void 0) {
3163
+ return isAbsolute(explicit) ? explicit : resolve(input.io.cwd, explicit);
3164
+ }
3165
+ if (input.rest.includes("--global")) {
3166
+ return join(input.io.homeDir, ".claude", "skills");
3167
+ }
3168
+ return resolve(input.io.cwd, ".claude", "skills");
3169
+ }
3170
+ function findSkill(name) {
3171
+ const target = name ?? DEFAULT_SKILL;
3172
+ return BUNDLED_SKILLS.find((skill) => skill.name === target);
3173
+ }
3174
+ function unknownSkill(name, json) {
3175
+ const available = BUNDLED_SKILLS.map((skill) => skill.name).join(", ");
3176
+ return errorResultWithCode(
3177
+ "validation_failure",
3178
+ `Unknown skill '${name}'. Available skills: ${available}.`,
3179
+ json
3180
+ );
3181
+ }
3182
+ function renderInstall(name, path, status2) {
3183
+ const verb = status2 === "created" ? "Installed" : "Updated";
3184
+ return [
3185
+ `${verb} the Sift skill: ${name}`,
3186
+ `Path: ${path}`,
3187
+ "",
3188
+ "Next, open that SKILL.md and follow it to finish setup:",
3189
+ " 1. Put the CLI on PATH: npm install -g @sift-wiki/cli",
3190
+ " 2. Authenticate: sift login",
3191
+ ' 3. Identify yourself: set SIFT_AGENT to your product name (e.g. "Claude Code"), then sift agent register',
3192
+ " 4. Confirm: sift doctor",
3193
+ "",
3194
+ "Then use Sift as your source of truth: search and assemble context before",
3195
+ "answering, and capture decisions and notes back into the brain.",
3196
+ ""
3197
+ ].join("\n");
3198
+ }
3199
+ function withTrailingNewline(markdown) {
3200
+ return markdown.endsWith("\n") ? markdown : `${markdown}
3201
+ `;
3202
+ }
3203
+
3204
+ // src/specialCommands.ts
3205
+ async function runSpecialCommand(input, args, json, group, command) {
3206
+ if (group === "doctor" && command === void 0) {
3207
+ return doctor({
3208
+ config: input.config,
3209
+ executor: input.executor,
3210
+ json,
3211
+ now: input.now ?? /* @__PURE__ */ new Date()
3212
+ });
3213
+ }
3214
+ if (group === "skill") {
3215
+ const io = input.skillIo ?? defaultSkillIo({ cwd: input.cwd, homeDir: input.homeDir });
3216
+ return runSkillCommand({ rest: args.slice(1), json, io });
3217
+ }
3218
+ const simpleCommand = resolveSimpleCommand({
3219
+ args,
3220
+ json,
3221
+ config: input.config,
3222
+ executor: input.executor,
3223
+ readFile: input.readFile ?? readFile,
3224
+ readStdin: input.readStdin,
3225
+ now: input.now ?? /* @__PURE__ */ new Date()
3226
+ });
3227
+ if (simpleCommand === void 0) return void 0;
3228
+ try {
3229
+ validateAuthenticatedScope(input.config, input.now ?? /* @__PURE__ */ new Date());
3230
+ validateCommandCapability({ commandKey: simpleCommand.commandKey, config: input.config });
3231
+ return await simpleCommand.run();
3232
+ } catch (error) {
3233
+ return errorResult(error, json);
3234
+ }
3235
+ }
3236
+
2599
3237
  // src/toolDiscovery.ts
2600
3238
  function toolsList(input) {
2601
3239
  if (input.executor !== void 0) {
@@ -2629,9 +3267,9 @@ function toolsHelp(input) {
2629
3267
  return executeSimple(input.executor, "tools.help", { name }, input.json);
2630
3268
  }
2631
3269
  async function executeSimple(executor, name, toolInput, json) {
2632
- const result2 = await executor.execute(name, toolInput);
2633
- return ok(json ? `${JSON.stringify(result2)}
2634
- ` : `${JSON.stringify(result2)}
3270
+ const result = await executor.execute(name, toolInput);
3271
+ return ok(json ? `${JSON.stringify(result)}
3272
+ ` : `${JSON.stringify(result)}
2635
3273
  `);
2636
3274
  }
2637
3275
 
@@ -2652,7 +3290,7 @@ function createHostedApiExecutor(input) {
2652
3290
  body: JSON.stringify({ input: toolInput }, serializeJsonValue)
2653
3291
  });
2654
3292
  const body = await response.text();
2655
- const parsed = body.length > 0 ? parseJson(body) : {};
3293
+ const parsed = body.length > 0 ? parseJson2(body) : {};
2656
3294
  if (!response.ok) {
2657
3295
  throw new Error(errorMessage(parsed, response.status));
2658
3296
  }
@@ -2669,7 +3307,7 @@ function serializeJsonValue(_key, value) {
2669
3307
  function toolUrl(apiBaseUrl, name) {
2670
3308
  return `${apiBaseUrl.replace(/\/+$/u, "")}/agent-tools/${encodeURIComponent(name)}`;
2671
3309
  }
2672
- function parseJson(body) {
3310
+ function parseJson2(body) {
2673
3311
  try {
2674
3312
  return JSON.parse(body);
2675
3313
  } catch {
@@ -2730,8 +3368,8 @@ async function runSiftCli(rawInput) {
2730
3368
  json
2731
3369
  }),
2732
3370
  "capture:text": () => captureText(input.executor, rest, json),
2733
- "capture:file": () => captureFile(input.executor, input.readFile ?? readFile, rest, json),
2734
- "capture:batch": () => captureBatch(input.executor, input.readFile ?? readFile, rest, json),
3371
+ "capture:file": () => captureFile(input.executor, input.readFile ?? readFile2, rest, json),
3372
+ "capture:batch": () => captureBatch(input.executor, input.readFile ?? readFile2, rest, json),
2735
3373
  "source:list": () => sourceList(input.executor, json),
2736
3374
  "source:create": () => sourceCreate(input.executor, rest, json),
2737
3375
  "source:get": () => sourceRead(input.executor, "get", rest, json),
@@ -2742,16 +3380,49 @@ async function runSiftCli(rawInput) {
2742
3380
  "record:create-markdown": () => createMarkdownRecord(input.executor, rest, json),
2743
3381
  "record:patch-section": () => patchRecordSection(input.executor, rest, json),
2744
3382
  "record:versions": () => recordRead(input.executor, "record.versions", rest, json),
2745
- "evidence:list": () => idTool({ executor: input.executor, toolName: "evidence.list", inputKey: "recordId", idLabel: "record ID", rest, json }),
2746
- "evidence:get": () => idTool({ executor: input.executor, toolName: "evidence.get", inputKey: "evidenceId", idLabel: "evidence ID", rest, json }),
2747
- "graph:neighbors": () => idTool({ executor: input.executor, toolName: "graph.neighbors", inputKey: "recordId", idLabel: "record ID", rest, json }),
3383
+ "evidence:list": () => idTool({
3384
+ executor: input.executor,
3385
+ toolName: "evidence.list",
3386
+ inputKey: "recordId",
3387
+ idLabel: "record ID",
3388
+ rest,
3389
+ json
3390
+ }),
3391
+ "evidence:get": () => idTool({
3392
+ executor: input.executor,
3393
+ toolName: "evidence.get",
3394
+ inputKey: "evidenceId",
3395
+ idLabel: "evidence ID",
3396
+ rest,
3397
+ json
3398
+ }),
3399
+ "graph:neighbors": () => idTool({
3400
+ executor: input.executor,
3401
+ toolName: "graph.neighbors",
3402
+ inputKey: "recordId",
3403
+ idLabel: "record ID",
3404
+ rest,
3405
+ json
3406
+ }),
2748
3407
  "event:list": () => executeSimple2(input.executor, "event.list", {}, json),
2749
3408
  "audit:events": () => auditEvents(input.executor, rest, json),
2750
3409
  "decision:create": () => createDecision(input.executor, rest, json),
2751
3410
  "task:create": () => createTask(input.executor, rest, json),
2752
3411
  "agent:register": () => agentRegister(input.executor, input.agentName, rest, json),
2753
3412
  "agent:status": () => executeSimple2(input.executor, "agent.status", {}, json),
2754
- "mcp:serve": () => mcpServe(input.mcpServer, input.config, rawInput.executor, json),
3413
+ "mcp:serve": () => mcpServe({
3414
+ mcpServer: input.mcpServer,
3415
+ config: input.config,
3416
+ executor: rawInput.executor
3417
+ }),
3418
+ "roam:import": () => runRoamImportCommand({
3419
+ rest,
3420
+ json,
3421
+ config: input.config,
3422
+ reader: input.roamReader,
3423
+ importer: input.roamImporter,
3424
+ now: input.now ?? /* @__PURE__ */ new Date()
3425
+ }),
2755
3426
  "login:": () => authCommand(input.authCommands, "login", { rest: commandRest, json }),
2756
3427
  "auth:status": () => authCommand(input.authCommands, "status", { json }),
2757
3428
  "logout:": () => authCommand(input.authCommands, "logout", { json })
@@ -2765,7 +3436,7 @@ async function runSiftCli(rawInput) {
2765
3436
  );
2766
3437
  }
2767
3438
  try {
2768
- if (isAuthCommand(commandKey)) {
3439
+ if (isAuthCommand(commandKey) || commandKey === "mcp:serve") {
2769
3440
  return await handler();
2770
3441
  }
2771
3442
  validateAuthenticatedScope(input.config, input.now ?? /* @__PURE__ */ new Date());
@@ -2775,61 +3446,14 @@ async function runSiftCli(rawInput) {
2775
3446
  return errorResult(error, json);
2776
3447
  }
2777
3448
  }
2778
- async function runSpecialCommand(input, args, json, group, command) {
2779
- if (group === "doctor" && command === void 0) {
2780
- return doctor({ config: input.config, executor: input.executor, json, now: input.now ?? /* @__PURE__ */ new Date() });
2781
- }
2782
- const simpleCommand = resolveSimpleCommand({
2783
- args,
2784
- json,
2785
- config: input.config,
2786
- executor: input.executor,
2787
- readFile: input.readFile ?? readFile,
2788
- readStdin: input.readStdin,
2789
- now: input.now ?? /* @__PURE__ */ new Date()
2790
- });
2791
- if (simpleCommand === void 0) return void 0;
2792
- try {
2793
- validateAuthenticatedScope(input.config, input.now ?? /* @__PURE__ */ new Date());
2794
- validateCommandCapability({ commandKey: simpleCommand.commandKey, config: input.config });
2795
- return await simpleCommand.run();
2796
- } catch (error) {
2797
- return errorResult(error, json);
2798
- }
2799
- }
2800
- async function mcpServe(mcpServer, config2, executor, json) {
2801
- if (mcpServer === void 0) {
2802
- return fail("No local MCP server is configured for mcp.serve.");
2803
- }
2804
- if (executor === void 0) {
2805
- return fail("No Sift API executor is configured for mcp.serve.");
2806
- }
2807
- const result2 = await mcpServer.serve({ config: config2, executor, transport: "local_mcp" });
2808
- if (result2 === void 0) return ok("");
2809
- return ok(`${JSON.stringify(result2)}
2810
- `);
2811
- }
2812
- function scopeCurrent(config2, json) {
2813
- const scope = {
2814
- apiBaseUrl: config2.apiBaseUrl,
2815
- tokenLabel: config2.tokenLabel,
2816
- tokenExpiresAt: config2.tokenExpiresAt,
2817
- principalId: config2.principalId,
2818
- workspaceId: config2.workspaceId,
2819
- brainId: config2.brainId,
2820
- capabilities: config2.capabilities
2821
- };
2822
- return ok(json ? `${JSON.stringify(scope)}
2823
- ` : renderScope(scope));
2824
- }
2825
3449
  async function searchQuery(executor, rest, json) {
2826
3450
  if (executor === void 0) {
2827
3451
  return fail("No Sift API executor is configured for search.query.");
2828
3452
  }
2829
3453
  const query = rest.join(" ").trim();
2830
- const result2 = await executor.execute("search.query", { query, limit: 10 });
2831
- return ok(json ? `${JSON.stringify(result2)}
2832
- ` : renderSearchResult(result2));
3454
+ const result = await executor.execute("search.query", { query, limit: 10 });
3455
+ return ok(json ? `${JSON.stringify(result)}
3456
+ ` : renderSearchResult(result));
2833
3457
  }
2834
3458
  async function contextAssemble(executor, rest, json) {
2835
3459
  if (executor === void 0) {
@@ -2837,12 +3461,12 @@ async function contextAssemble(executor, rest, json) {
2837
3461
  }
2838
3462
  const parsed = parseOptions(rest);
2839
3463
  const query = positionalArgs(rest).join(" ").trim();
2840
- const result2 = await executor.execute("context.assemble", {
3464
+ const result = await executor.execute("context.assemble", {
2841
3465
  query,
2842
3466
  maxChars: parseIntegerOption(parsed, "max-chars", 4e3)
2843
3467
  });
2844
- return ok(json ? `${JSON.stringify(result2)}
2845
- ` : renderSearchResult(result2));
3468
+ return ok(json ? `${JSON.stringify(result)}
3469
+ ` : renderSearchResult(result));
2846
3470
  }
2847
3471
  async function contextProfile(executor, rest, json) {
2848
3472
  if (executor === void 0) {
@@ -2856,17 +3480,17 @@ async function contextProfile(executor, rest, json) {
2856
3480
  if (query.length > 0) {
2857
3481
  input.query = query;
2858
3482
  }
2859
- const result2 = await executor.execute("context.profile", input);
2860
- return ok(json ? `${JSON.stringify(result2)}
2861
- ` : renderProfileResult(result2));
3483
+ const result = await executor.execute("context.profile", input);
3484
+ return ok(json ? `${JSON.stringify(result)}
3485
+ ` : renderProfileResult(result));
2862
3486
  }
2863
3487
  async function executeSimple2(executor, name, toolInput, json) {
2864
3488
  if (executor === void 0) {
2865
3489
  return fail(`No Sift API executor is configured for ${name}.`);
2866
3490
  }
2867
- const result2 = await executor.execute(name, toolInput);
2868
- return ok(json ? `${JSON.stringify(result2)}
2869
- ` : `${JSON.stringify(result2)}
3491
+ const result = await executor.execute(name, toolInput);
3492
+ return ok(json ? `${JSON.stringify(result)}
3493
+ ` : `${JSON.stringify(result)}
2870
3494
  `);
2871
3495
  }
2872
3496
  async function captureText(executor, rest, json) {
@@ -2874,15 +3498,15 @@ async function captureText(executor, rest, json) {
2874
3498
  return fail("No Sift API executor is configured for capture.text.");
2875
3499
  }
2876
3500
  const parsed = parseOptions(rest);
2877
- const result2 = await executor.execute("capture.text", {
3501
+ const result = await executor.execute("capture.text", {
2878
3502
  sourceName: requireOption(parsed, "source"),
2879
3503
  externalId: requireOption(parsed, "external-id"),
2880
3504
  title: requireOption(parsed, "title"),
2881
3505
  visibility: [requireOption(parsed, "visibility")],
2882
3506
  markdown: requireOption(parsed, "markdown")
2883
3507
  });
2884
- return ok(json ? `${JSON.stringify(result2)}
2885
- ` : `${JSON.stringify(result2)}
3508
+ return ok(json ? `${JSON.stringify(result)}
3509
+ ` : `${JSON.stringify(result)}
2886
3510
  `);
2887
3511
  }
2888
3512
  async function captureFile(executor, fileReader, rest, json) {
@@ -2895,7 +3519,7 @@ async function captureFile(executor, fileReader, rest, json) {
2895
3519
  }
2896
3520
  const parsed = parseOptions(optionArgs);
2897
3521
  const bytes = await fileReader(path);
2898
- const result2 = await executor.execute("capture.file", {
3522
+ const result = await executor.execute("capture.file", {
2899
3523
  sourceName: requireOption(parsed, "source"),
2900
3524
  externalId: requireOption(parsed, "external-id"),
2901
3525
  title: requireOption(parsed, "title"),
@@ -2904,8 +3528,8 @@ async function captureFile(executor, fileReader, rest, json) {
2904
3528
  bytes,
2905
3529
  visibility: [requireOption(parsed, "visibility")]
2906
3530
  });
2907
- return ok(json ? `${JSON.stringify(result2)}
2908
- ` : `${JSON.stringify(result2)}
3531
+ return ok(json ? `${JSON.stringify(result)}
3532
+ ` : `${JSON.stringify(result)}
2909
3533
  `);
2910
3534
  }
2911
3535
  async function captureBatch(executor, fileReader, rest, json) {
@@ -2917,9 +3541,9 @@ async function captureBatch(executor, fileReader, rest, json) {
2917
3541
  return fail("Missing required manifest path for capture.batch.");
2918
3542
  }
2919
3543
  const manifest = parseBatchManifest(await fileReader(manifestPath));
2920
- const result2 = await executor.execute("capture.batch", { items: manifest });
2921
- return ok(json ? `${JSON.stringify(result2)}
2922
- ` : `${JSON.stringify(result2)}
3544
+ const result = await executor.execute("capture.batch", { items: manifest });
3545
+ return ok(json ? `${JSON.stringify(result)}
3546
+ ` : `${JSON.stringify(result)}
2923
3547
  `);
2924
3548
  }
2925
3549
  function parseBatchManifest(bytes) {
@@ -2944,18 +3568,18 @@ async function createDecision(executor, rest, json) {
2944
3568
  input.rationale = rationale;
2945
3569
  }
2946
3570
  addOptionalWorkMetadata(input, parsed);
2947
- const result2 = await executor.execute("decision.create", input);
2948
- return ok(json ? `${JSON.stringify(result2)}
2949
- ` : `${JSON.stringify(result2)}
3571
+ const result = await executor.execute("decision.create", input);
3572
+ return ok(json ? `${JSON.stringify(result)}
3573
+ ` : `${JSON.stringify(result)}
2950
3574
  `);
2951
3575
  }
2952
3576
  async function sourceList(executor, json) {
2953
3577
  if (executor === void 0) {
2954
3578
  return fail("No Sift API executor is configured for source.list.");
2955
3579
  }
2956
- const result2 = await executor.execute("source.list", {});
2957
- return ok(json ? `${JSON.stringify(result2)}
2958
- ` : `${JSON.stringify(result2)}
3580
+ const result = await executor.execute("source.list", {});
3581
+ return ok(json ? `${JSON.stringify(result)}
3582
+ ` : `${JSON.stringify(result)}
2959
3583
  `);
2960
3584
  }
2961
3585
  async function sourceCreate(executor, rest, json) {
@@ -2963,12 +3587,12 @@ async function sourceCreate(executor, rest, json) {
2963
3587
  return fail("No Sift API executor is configured for source.create.");
2964
3588
  }
2965
3589
  const parsed = parseOptions(rest);
2966
- const result2 = await executor.execute("source.create", {
3590
+ const result = await executor.execute("source.create", {
2967
3591
  name: requireOption(parsed, "name"),
2968
3592
  visibility: [requireOption(parsed, "visibility")]
2969
3593
  });
2970
- return ok(json ? `${JSON.stringify(result2)}
2971
- ` : `${JSON.stringify(result2)}
3594
+ return ok(json ? `${JSON.stringify(result)}
3595
+ ` : `${JSON.stringify(result)}
2972
3596
  `);
2973
3597
  }
2974
3598
  async function sourceRead(executor, command, rest, json) {
@@ -2980,9 +3604,9 @@ async function sourceRead(executor, command, rest, json) {
2980
3604
  if (sourceId === void 0 || sourceId.trim().length === 0) {
2981
3605
  return fail(`Missing required source ID for ${toolName}.`);
2982
3606
  }
2983
- const result2 = await executor.execute(toolName, { sourceId });
2984
- return ok(json ? `${JSON.stringify(result2)}
2985
- ` : `${JSON.stringify(result2)}
3607
+ const result = await executor.execute(toolName, { sourceId });
3608
+ return ok(json ? `${JSON.stringify(result)}
3609
+ ` : `${JSON.stringify(result)}
2986
3610
  `);
2987
3611
  }
2988
3612
  async function ingestionStatus(executor, rest, json) {
@@ -2993,18 +3617,18 @@ async function ingestionStatus(executor, rest, json) {
2993
3617
  if (jobId === void 0 || jobId.trim().length === 0) {
2994
3618
  return fail("Missing required job ID for ingestion.status.");
2995
3619
  }
2996
- const result2 = await executor.execute("ingestion.status", { jobId });
2997
- return ok(json ? `${JSON.stringify(result2)}
2998
- ` : `${JSON.stringify(result2)}
3620
+ const result = await executor.execute("ingestion.status", { jobId });
3621
+ return ok(json ? `${JSON.stringify(result)}
3622
+ ` : `${JSON.stringify(result)}
2999
3623
  `);
3000
3624
  }
3001
3625
  async function recordList(executor, json) {
3002
3626
  if (executor === void 0) {
3003
3627
  return fail("No Sift API executor is configured for record.list.");
3004
3628
  }
3005
- const result2 = await executor.execute("record.list", {});
3006
- return ok(json ? `${JSON.stringify(result2)}
3007
- ` : `${JSON.stringify(result2)}
3629
+ const result = await executor.execute("record.list", {});
3630
+ return ok(json ? `${JSON.stringify(result)}
3631
+ ` : `${JSON.stringify(result)}
3008
3632
  `);
3009
3633
  }
3010
3634
  async function recordRead(executor, toolName, rest, json) {
@@ -3022,23 +3646,23 @@ async function recordRead(executor, toolName, rest, json) {
3022
3646
  input.sectionAnchor = sectionAnchor;
3023
3647
  }
3024
3648
  }
3025
- const result2 = await executor.execute(toolName, input);
3026
- return ok(json ? `${JSON.stringify(result2)}
3027
- ` : renderRecordResult(result2));
3649
+ const result = await executor.execute(toolName, input);
3650
+ return ok(json ? `${JSON.stringify(result)}
3651
+ ` : renderRecordResult(result));
3028
3652
  }
3029
3653
  async function createMarkdownRecord(executor, rest, json) {
3030
3654
  if (executor === void 0) {
3031
3655
  return fail("No Sift API executor is configured for record.create_markdown.");
3032
3656
  }
3033
3657
  const parsed = parseOptions(rest);
3034
- const result2 = await executor.execute("record.create_markdown", {
3658
+ const result = await executor.execute("record.create_markdown", {
3035
3659
  recordType: requireOption(parsed, "type"),
3036
3660
  title: requireOption(parsed, "title"),
3037
3661
  markdown: requireOption(parsed, "markdown"),
3038
3662
  visibility: [requireOption(parsed, "visibility")]
3039
3663
  });
3040
- return ok(json ? `${JSON.stringify(result2)}
3041
- ` : `${JSON.stringify(result2)}
3664
+ return ok(json ? `${JSON.stringify(result)}
3665
+ ` : `${JSON.stringify(result)}
3042
3666
  `);
3043
3667
  }
3044
3668
  async function patchRecordSection(executor, rest, json) {
@@ -3062,9 +3686,9 @@ async function patchRecordSection(executor, rest, json) {
3062
3686
  if (expectedMarkdown !== void 0) {
3063
3687
  input.expectedMarkdown = expectedMarkdown;
3064
3688
  }
3065
- const result2 = await executor.execute("record.patch_section", input);
3066
- return ok(json ? `${JSON.stringify(result2)}
3067
- ` : `${JSON.stringify(result2)}
3689
+ const result = await executor.execute("record.patch_section", input);
3690
+ return ok(json ? `${JSON.stringify(result)}
3691
+ ` : `${JSON.stringify(result)}
3068
3692
  `);
3069
3693
  }
3070
3694
  async function createTask(executor, rest, json) {
@@ -3093,9 +3717,9 @@ async function createTask(executor, rest, json) {
3093
3717
  input.rationale = rationale;
3094
3718
  }
3095
3719
  addOptionalWorkMetadata(input, parsed);
3096
- const result2 = await executor.execute("task.create", input);
3097
- return ok(json ? `${JSON.stringify(result2)}
3098
- ` : `${JSON.stringify(result2)}
3720
+ const result = await executor.execute("task.create", input);
3721
+ return ok(json ? `${JSON.stringify(result)}
3722
+ ` : `${JSON.stringify(result)}
3099
3723
  `);
3100
3724
  }
3101
3725
  async function auditEvents(executor, rest, json) {
@@ -3107,9 +3731,9 @@ async function auditEvents(executor, rest, json) {
3107
3731
  if (targetId !== void 0 && targetId.trim().length > 0) {
3108
3732
  input.targetId = targetId;
3109
3733
  }
3110
- const result2 = await executor.execute("audit.events", input);
3111
- return ok(json ? `${JSON.stringify(result2)}
3112
- ` : `${JSON.stringify(result2)}
3734
+ const result = await executor.execute("audit.events", input);
3735
+ return ok(json ? `${JSON.stringify(result)}
3736
+ ` : `${JSON.stringify(result)}
3113
3737
  `);
3114
3738
  }
3115
3739
  async function idTool(input) {
@@ -3120,22 +3744,25 @@ async function idTool(input) {
3120
3744
  if (id === void 0 || id.trim().length === 0) {
3121
3745
  return fail(`Missing required ${input.idLabel} for ${input.toolName}.`);
3122
3746
  }
3123
- const result2 = await input.executor.execute(input.toolName, { [input.inputKey]: id });
3124
- return ok(input.json ? `${JSON.stringify(result2)}
3125
- ` : `${JSON.stringify(result2)}
3747
+ const result = await input.executor.execute(input.toolName, { [input.inputKey]: id });
3748
+ return ok(input.json ? `${JSON.stringify(result)}
3749
+ ` : `${JSON.stringify(result)}
3126
3750
  `);
3127
3751
  }
3128
3752
 
3129
3753
  // src/auth/configStore.ts
3130
- import { mkdir, readFile as readFile2, rm, writeFile, chmod } from "fs/promises";
3131
- import { dirname, join } from "path";
3754
+ import { mkdir, readFile as readFile3, rm, writeFile, chmod } from "fs/promises";
3755
+ import { dirname, join as join2 } from "path";
3756
+ function refreshSlotTokenId(tokenId) {
3757
+ return `refresh:${tokenId}`;
3758
+ }
3132
3759
  function resolveSiftConfigPath(input) {
3133
- return join(input.homeDir, ".sift", "config.json");
3760
+ return join2(input.homeDir, ".sift", "config.json");
3134
3761
  }
3135
3762
  async function readStoredSiftConfig(input) {
3136
3763
  let raw;
3137
3764
  try {
3138
- raw = await readFile2(resolveSiftConfigPath(input), "utf8");
3765
+ raw = await readFile3(resolveSiftConfigPath(input), "utf8");
3139
3766
  } catch (error) {
3140
3767
  if (isNodeError(error) && error.code === "ENOENT") {
3141
3768
  return void 0;
@@ -3145,10 +3772,10 @@ async function readStoredSiftConfig(input) {
3145
3772
  return parseStoredSiftConfig(JSON.parse(raw));
3146
3773
  }
3147
3774
  async function writeStoredSiftConfig(input) {
3148
- const config2 = parseStoredSiftConfig(input.config);
3775
+ const config = parseStoredSiftConfig(input.config);
3149
3776
  const path = resolveSiftConfigPath(input);
3150
3777
  await mkdir(dirname(path), { recursive: true, mode: 448 });
3151
- await writeFile(path, `${JSON.stringify(config2, null, 2)}
3778
+ await writeFile(path, `${JSON.stringify(config, null, 2)}
3152
3779
  `, { mode: 384 });
3153
3780
  await chmod(path, 384);
3154
3781
  }
@@ -3168,18 +3795,19 @@ async function loadCliAuthConfig(input) {
3168
3795
  if (profile === void 0) {
3169
3796
  throw new Error(`Stored Sift profile '${stored.currentProfile}' was not found.`);
3170
3797
  }
3171
- if (Date.parse(profile.tokenExpiresAt) <= input.now.getTime()) {
3172
- throw new Error("Stored Sift CLI auth has expired; run `sift login` again.");
3173
- }
3174
- const token = await input.credentialStore.read({
3175
- apiBaseUrl: profile.apiBaseUrl,
3176
- tokenId: profile.tokenId
3798
+ const tokenKind = profile.tokenKind ?? "legacy";
3799
+ const expired = Date.parse(profile.tokenExpiresAt) <= input.now.getTime();
3800
+ const token = await resolveStoredToken({
3801
+ homeDir: input.homeDir,
3802
+ credentialStore: input.credentialStore,
3803
+ profile,
3804
+ tokenKind,
3805
+ expired,
3806
+ oauthRefresher: input.oauthRefresher
3177
3807
  });
3178
- if (token === void 0) {
3179
- throw new Error("Stored Sift credential store secret is missing; run `sift login` again.");
3180
- }
3181
3808
  return {
3182
3809
  source: "stored",
3810
+ tokenKind,
3183
3811
  token,
3184
3812
  config: {
3185
3813
  apiBaseUrl: profile.apiBaseUrl,
@@ -3192,24 +3820,81 @@ async function loadCliAuthConfig(input) {
3192
3820
  }
3193
3821
  };
3194
3822
  }
3195
- function loadEnvAuth(env, token) {
3196
- return {
3197
- source: "env",
3198
- token,
3199
- config: {
3200
- apiBaseUrl: requiredEnv(env, "SIFT_API_BASE_URL").replace(/\/+$/u, ""),
3201
- tokenLabel: clean(env.SIFT_TOKEN_LABEL) ?? "env-token",
3202
- tokenExpiresAt: clean(env.SIFT_TOKEN_EXPIRES_AT),
3203
- workspaceId: requiredEnv(env, "SIFT_WORKSPACE_ID"),
3204
- brainId: requiredEnv(env, "SIFT_BRAIN_ID"),
3205
- principalId: requiredEnv(env, "SIFT_PRINCIPAL_ID"),
3206
- capabilities: (clean(env.SIFT_TOKEN_CAPABILITIES) ?? "").split(",").map((item) => item.trim()).filter((item) => item.length > 0)
3207
- }
3208
- };
3209
- }
3210
- function parseStoredSiftConfig(value) {
3211
- const record = objectValue(value, "config");
3212
- const currentProfile = stringValue(record.currentProfile, "currentProfile");
3823
+ async function resolveStoredToken(input) {
3824
+ const { profile } = input;
3825
+ if (input.expired && input.tokenKind === "oauth" && profile.refreshable === true && input.oauthRefresher !== void 0) {
3826
+ return refreshOAuthToken({
3827
+ homeDir: input.homeDir,
3828
+ credentialStore: input.credentialStore,
3829
+ profile,
3830
+ oauthRefresher: input.oauthRefresher
3831
+ });
3832
+ }
3833
+ if (input.expired) {
3834
+ throw new Error("Stored Sift CLI auth has expired; run `sift login` again.");
3835
+ }
3836
+ const token = await input.credentialStore.read({
3837
+ apiBaseUrl: profile.apiBaseUrl,
3838
+ tokenId: profile.tokenId
3839
+ });
3840
+ if (token === void 0) {
3841
+ throw new Error("Stored Sift credential store secret is missing; run `sift login` again.");
3842
+ }
3843
+ return token;
3844
+ }
3845
+ async function refreshOAuthToken(input) {
3846
+ const { profile } = input;
3847
+ const refreshToken = await input.credentialStore.read({
3848
+ apiBaseUrl: profile.apiBaseUrl,
3849
+ tokenId: refreshSlotTokenId(profile.tokenId)
3850
+ });
3851
+ if (refreshToken === void 0) {
3852
+ throw new Error("Stored Sift OAuth refresh token is missing; run `sift login` again.");
3853
+ }
3854
+ const refreshed = await input.oauthRefresher({
3855
+ apiBaseUrl: profile.apiBaseUrl,
3856
+ refreshToken
3857
+ });
3858
+ await input.credentialStore.write({
3859
+ apiBaseUrl: profile.apiBaseUrl,
3860
+ tokenId: profile.tokenId,
3861
+ secret: refreshed.accessToken
3862
+ });
3863
+ if (refreshed.refreshToken !== void 0) {
3864
+ await input.credentialStore.write({
3865
+ apiBaseUrl: profile.apiBaseUrl,
3866
+ tokenId: refreshSlotTokenId(profile.tokenId),
3867
+ secret: refreshed.refreshToken
3868
+ });
3869
+ }
3870
+ const updated = {
3871
+ ...profile,
3872
+ tokenExpiresAt: refreshed.expiresAt ?? profile.tokenExpiresAt
3873
+ };
3874
+ await writeStoredSiftConfig({
3875
+ homeDir: input.homeDir,
3876
+ config: { currentProfile: "default", profiles: { default: updated } }
3877
+ });
3878
+ return refreshed.accessToken;
3879
+ }
3880
+ function loadEnvAuth(env, token) {
3881
+ return {
3882
+ source: "env",
3883
+ token,
3884
+ config: {
3885
+ apiBaseUrl: requiredEnv(env, "SIFT_API_BASE_URL").replace(/\/+$/u, ""),
3886
+ tokenLabel: clean(env.SIFT_TOKEN_LABEL) ?? "env-token",
3887
+ tokenExpiresAt: clean(env.SIFT_TOKEN_EXPIRES_AT),
3888
+ workspaceId: requiredEnv(env, "SIFT_WORKSPACE_ID"),
3889
+ brainId: requiredEnv(env, "SIFT_BRAIN_ID"),
3890
+ principalId: requiredEnv(env, "SIFT_PRINCIPAL_ID"),
3891
+ capabilities: (clean(env.SIFT_TOKEN_CAPABILITIES) ?? "").split(",").map((item) => item.trim()).filter((item) => item.length > 0)
3892
+ }
3893
+ };
3894
+ }
3895
+ function parseStoredSiftConfig(value) {
3896
+ const record = objectValue(value, "config");
3897
+ const currentProfile = stringValue(record.currentProfile, "currentProfile");
3213
3898
  const profilesRecord = objectValue(record.profiles, "profiles");
3214
3899
  const profiles = {};
3215
3900
  for (const [name, profileValue] of Object.entries(profilesRecord)) {
@@ -3222,10 +3907,10 @@ function parseStoredSiftConfig(value) {
3222
3907
  }
3223
3908
  function parseStoredSiftProfile(value) {
3224
3909
  const record = objectValue(value, "profile");
3225
- if ("token" in record || "secret" in record || "tokenSecret" in record) {
3910
+ if ("token" in record || "secret" in record || "tokenSecret" in record || "accessToken" in record || "refreshToken" in record) {
3226
3911
  throw new Error("Stored Sift config must not contain token secrets.");
3227
3912
  }
3228
- return {
3913
+ const profile = {
3229
3914
  apiBaseUrl: stringValue(record.apiBaseUrl, "apiBaseUrl").replace(/\/+$/u, ""),
3230
3915
  appBaseUrl: stringValue(record.appBaseUrl, "appBaseUrl").replace(/\/+$/u, ""),
3231
3916
  workspaceId: stringValue(record.workspaceId, "workspaceId"),
@@ -3236,6 +3921,21 @@ function parseStoredSiftProfile(value) {
3236
3921
  tokenExpiresAt: stringValue(record.tokenExpiresAt, "tokenExpiresAt"),
3237
3922
  capabilities: stringArray(record.capabilities, "capabilities")
3238
3923
  };
3924
+ const tokenKind = tokenKindValue(record.tokenKind);
3925
+ if (tokenKind !== void 0) {
3926
+ profile.tokenKind = tokenKind;
3927
+ }
3928
+ if (record.refreshable === true) {
3929
+ profile.refreshable = true;
3930
+ }
3931
+ return profile;
3932
+ }
3933
+ function tokenKindValue(value) {
3934
+ if (value === void 0) return void 0;
3935
+ if (value === "legacy" || value === "oauth" || value === "service") {
3936
+ return value;
3937
+ }
3938
+ throw new Error("tokenKind must be one of legacy, oauth, service.");
3239
3939
  }
3240
3940
  function requiredEnv(env, name) {
3241
3941
  const value = clean(env[name]);
@@ -3296,14 +3996,14 @@ function createMacOSKeychainStore(input = {}) {
3296
3996
  return {
3297
3997
  async assertAvailable() {
3298
3998
  await requireSupported();
3299
- const result2 = await runCommand(securityPath, ["list-keychains"]);
3300
- if (result2.exitCode !== 0) {
3999
+ const result = await runCommand(securityPath, ["list-keychains"]);
4000
+ if (result.exitCode !== 0) {
3301
4001
  throw new UnsupportedCredentialStoreError();
3302
4002
  }
3303
4003
  },
3304
4004
  async read(readInput) {
3305
4005
  await requireSupported();
3306
- const result2 = await runCommand(securityPath, [
4006
+ const result = await runCommand(securityPath, [
3307
4007
  "find-generic-password",
3308
4008
  "-s",
3309
4009
  serviceName,
@@ -3311,15 +4011,15 @@ function createMacOSKeychainStore(input = {}) {
3311
4011
  account(readInput),
3312
4012
  "-w"
3313
4013
  ]);
3314
- if (result2.exitCode !== 0) {
4014
+ if (result.exitCode !== 0) {
3315
4015
  return void 0;
3316
4016
  }
3317
- const secret = result2.stdout.trim();
4017
+ const secret = result.stdout.trim();
3318
4018
  return secret.length === 0 ? void 0 : secret;
3319
4019
  },
3320
4020
  async write(writeInput) {
3321
4021
  await requireSupported();
3322
- const result2 = await runCommand(securityPath, [
4022
+ const result = await runCommand(securityPath, [
3323
4023
  "add-generic-password",
3324
4024
  "-U",
3325
4025
  "-s",
@@ -3329,7 +4029,7 @@ function createMacOSKeychainStore(input = {}) {
3329
4029
  "-w",
3330
4030
  writeInput.secret
3331
4031
  ]);
3332
- if (result2.exitCode !== 0) {
4032
+ if (result.exitCode !== 0) {
3333
4033
  throw new Error("Failed to write Sift CLI token secret to macOS Keychain.");
3334
4034
  }
3335
4035
  },
@@ -3347,8 +4047,8 @@ function createMacOSKeychainStore(input = {}) {
3347
4047
  }
3348
4048
  async function runSecurityCommand(file, args) {
3349
4049
  try {
3350
- const result2 = await execFileAsync(file, args);
3351
- return { stdout: result2.stdout, stderr: result2.stderr, exitCode: 0 };
4050
+ const result = await execFileAsync(file, args);
4051
+ return { stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
3352
4052
  } catch (error) {
3353
4053
  if (isExecError(error)) {
3354
4054
  return {
@@ -3369,21 +4069,116 @@ function isExecError(error) {
3369
4069
 
3370
4070
  // src/auth/loginFlow.ts
3371
4071
  import { execFile as execFile2 } from "child_process";
3372
- import { hostname } from "os";
4072
+ import { hostname as hostname3 } from "os";
3373
4073
  import { promisify as promisify2 } from "util";
3374
4074
 
4075
+ // src/auth/loginHelpers.ts
4076
+ var DEFAULT_SIFT_API_BASE_URL = "https://sift-wiki-api.fly.dev";
4077
+ async function resolveLoginApiBaseUrl(input) {
4078
+ const options = parseOptions(input.argv);
4079
+ const fromFlag = clean2(options.get("api-base-url"));
4080
+ if (fromFlag !== void 0) return normalizeUrl(fromFlag);
4081
+ const fromEnv = clean2(input.env.SIFT_API_BASE_URL);
4082
+ if (fromEnv !== void 0) return normalizeUrl(fromEnv);
4083
+ const stored = await readStoredSiftConfig({ homeDir: input.homeDir });
4084
+ const profile = stored?.profiles[stored.currentProfile];
4085
+ if (profile !== void 0) return normalizeUrl(profile.apiBaseUrl);
4086
+ return DEFAULT_SIFT_API_BASE_URL;
4087
+ }
4088
+ function requestedCapabilities(rest) {
4089
+ const option = parseOptions(rest).get("capability");
4090
+ return option === void 0 ? ["record:read"] : option.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
4091
+ }
4092
+ function errorMessage2(parsed, status2) {
4093
+ if (typeof parsed === "object" && parsed !== null && "error" in parsed) {
4094
+ const error = parsed.error;
4095
+ if (typeof error === "object" && error !== null && "message" in error) {
4096
+ const message = error.message;
4097
+ if (typeof message === "string") return message;
4098
+ }
4099
+ }
4100
+ return `CLI auth request failed with status ${status2}.`;
4101
+ }
4102
+ function normalizeUrl(value) {
4103
+ return value.replace(/\/+$/u, "");
4104
+ }
4105
+ function clean2(value) {
4106
+ const trimmed = value?.trim();
4107
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
4108
+ }
4109
+
4110
+ // src/auth/oauthConfig.ts
4111
+ var TRUE_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
4112
+ function oauthLoginSelected(input) {
4113
+ if (input.argv.includes("--no-oauth")) return false;
4114
+ if (input.argv.includes("--oauth")) return true;
4115
+ const fromEnv = input.env.SIFT_OAUTH?.trim().toLowerCase();
4116
+ return fromEnv !== void 0 && TRUE_VALUES.has(fromEnv);
4117
+ }
4118
+ function resolveCliOAuthConfig(input) {
4119
+ const options = parseOptions(input.argv);
4120
+ const authorizeUrl = clean3(options.get("oauth-authorize-url")) ?? clean3(input.env.SIFT_OAUTH_AUTHORIZE_URL);
4121
+ const tokenUrl = clean3(options.get("oauth-token-url")) ?? clean3(input.env.SIFT_OAUTH_TOKEN_URL);
4122
+ const clientId = clean3(options.get("oauth-client-id")) ?? clean3(input.env.SIFT_OAUTH_CLIENT_ID);
4123
+ if (authorizeUrl === void 0 || tokenUrl === void 0 || clientId === void 0) {
4124
+ return void 0;
4125
+ }
4126
+ const registrationUrl = clean3(options.get("oauth-registration-url")) ?? clean3(input.env.SIFT_OAUTH_REGISTRATION_URL);
4127
+ const config = { authorizeUrl, tokenUrl, clientId };
4128
+ if (registrationUrl !== void 0) {
4129
+ config.registrationUrl = registrationUrl;
4130
+ }
4131
+ const scopes = parseScopeList(clean3(options.get("oauth-scopes")) ?? clean3(input.env.SIFT_OAUTH_SCOPES));
4132
+ if (scopes.length > 0) {
4133
+ config.defaultScopes = scopes;
4134
+ }
4135
+ return config;
4136
+ }
4137
+ function scopesForCapabilities(capabilities) {
4138
+ const scopes = /* @__PURE__ */ new Set(["read"]);
4139
+ for (const capability of capabilities) {
4140
+ if (capability.endsWith(":write")) {
4141
+ scopes.add("write");
4142
+ }
4143
+ }
4144
+ return [...scopes];
4145
+ }
4146
+ function parseScopeList(value) {
4147
+ if (value === void 0) return [];
4148
+ return value.split(/[\s,]+/u).map((item) => item.trim()).filter((item) => item.length > 0);
4149
+ }
4150
+ function clean3(value) {
4151
+ const trimmed = value?.trim();
4152
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
4153
+ }
4154
+
4155
+ // src/auth/oauthLoginFlow.ts
4156
+ import { hostname } from "os";
4157
+
3375
4158
  // src/auth/localCallback.ts
3376
4159
  import { createServer } from "http";
3377
4160
  async function createLocalCallbackServer() {
3378
4161
  let resolveCallback;
3379
4162
  let rejectCallback;
3380
- const callbackPromise = new Promise((resolve, reject) => {
3381
- resolveCallback = resolve;
4163
+ const callbackPromise = new Promise((resolve2, reject) => {
4164
+ resolveCallback = resolve2;
3382
4165
  rejectCallback = reject;
3383
4166
  });
3384
4167
  const server = createServer((request, response) => {
3385
4168
  try {
3386
4169
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
4170
+ const error = url.searchParams.get("error");
4171
+ if (error !== null) {
4172
+ const description = url.searchParams.get("error_description");
4173
+ response.writeHead(400, { "Content-Type": "text/plain" });
4174
+ response.end("Sift CLI authorization failed. You can return to the terminal.");
4175
+ rejectCallback?.(
4176
+ new Error(
4177
+ description === null || description.trim().length === 0 ? `Authorization failed: ${error}.` : `Authorization failed: ${error}: ${description}`
4178
+ )
4179
+ );
4180
+ return;
4181
+ }
3387
4182
  const code = url.searchParams.get("code");
3388
4183
  const state = url.searchParams.get("state");
3389
4184
  if (code === null || state === null) {
@@ -3411,21 +4206,21 @@ async function createLocalCallbackServer() {
3411
4206
  };
3412
4207
  }
3413
4208
  function listen(server) {
3414
- return new Promise((resolve, reject) => {
4209
+ return new Promise((resolve2, reject) => {
3415
4210
  server.once("error", reject);
3416
4211
  server.listen(0, "127.0.0.1", () => {
3417
4212
  server.off("error", reject);
3418
- resolve();
4213
+ resolve2();
3419
4214
  });
3420
4215
  });
3421
4216
  }
3422
4217
  function closeServer(server) {
3423
- return new Promise((resolve, reject) => {
4218
+ return new Promise((resolve2, reject) => {
3424
4219
  server.close((error) => {
3425
4220
  if (error) {
3426
4221
  reject(error);
3427
4222
  } else {
3428
- resolve();
4223
+ resolve2();
3429
4224
  }
3430
4225
  });
3431
4226
  });
@@ -3451,15 +4246,445 @@ function sha256Base64Url(value) {
3451
4246
  return createHash2("sha256").update(value).digest("base64url");
3452
4247
  }
3453
4248
 
4249
+ // src/auth/oauthLoginFlow.ts
4250
+ async function oauthBrowserLogin(input) {
4251
+ await input.credentialStore.assertAvailable();
4252
+ const callbackServer = await (input.createCallbackServer ?? createLocalCallbackServer)();
4253
+ try {
4254
+ const pkce = createPkceState({ nextSecret: input.nextSecret });
4255
+ const scopes = mergeScopes(scopesForCapabilities(input.capabilities), input.oauth.defaultScopes);
4256
+ const authorizeUrl = buildAuthorizeUrl({
4257
+ oauth: input.oauth,
4258
+ redirectUri: callbackServer.redirectUri,
4259
+ codeChallenge: pkce.codeChallenge,
4260
+ state: pkce.state,
4261
+ scopes
4262
+ });
4263
+ await tryOpenBrowser(input.openBrowser, authorizeUrl);
4264
+ const callback = await callbackServer.waitForCallback();
4265
+ if (callback.state !== pkce.state) {
4266
+ throw new Error("OAuth callback state mismatch.");
4267
+ }
4268
+ const tokens = await exchangeAuthorizationCode({
4269
+ oauth: input.oauth,
4270
+ fetch: input.fetch,
4271
+ code: callback.code,
4272
+ codeVerifier: pkce.codeVerifier,
4273
+ redirectUri: callbackServer.redirectUri
4274
+ });
4275
+ return finalizeOAuthLogin(input, tokens);
4276
+ } finally {
4277
+ await callbackServer.close();
4278
+ }
4279
+ }
4280
+ async function oauthRefresh(input) {
4281
+ const body = new URLSearchParams({
4282
+ grant_type: "refresh_token",
4283
+ refresh_token: input.refreshToken,
4284
+ client_id: input.oauth.clientId
4285
+ });
4286
+ const tokens = await postForm(input.fetch, input.oauth.tokenUrl, body);
4287
+ return toTokenSet(tokens);
4288
+ }
4289
+ async function finalizeOAuthLogin(input, tokens) {
4290
+ const scope = await input.resolveScope({
4291
+ apiBaseUrl: input.apiBaseUrl,
4292
+ token: tokens.accessToken,
4293
+ fetch: input.fetch
4294
+ });
4295
+ const profile = {
4296
+ apiBaseUrl: input.apiBaseUrl,
4297
+ appBaseUrl: input.appBaseUrl,
4298
+ workspaceId: scope.workspaceId,
4299
+ brainId: scope.brainId,
4300
+ principalId: scope.principalId,
4301
+ // Synthetic, non-secret slot id so the converged token reuses the same
4302
+ // keychain account scheme (apiBaseUrl|tokenId) as the legacy flow.
4303
+ tokenId: "oauth",
4304
+ tokenLabel: tokens.tokenLabel,
4305
+ tokenExpiresAt: tokens.expiresAt ?? farFuture(),
4306
+ capabilities: scope.capabilities,
4307
+ tokenKind: "oauth",
4308
+ refreshable: tokens.refreshToken !== void 0
4309
+ };
4310
+ const result = { profile, accessToken: tokens.accessToken };
4311
+ if (tokens.refreshToken !== void 0) {
4312
+ result.refreshToken = tokens.refreshToken;
4313
+ }
4314
+ return result;
4315
+ }
4316
+ function buildAuthorizeUrl(input) {
4317
+ const url = new URL(input.oauth.authorizeUrl);
4318
+ url.searchParams.set("response_type", "code");
4319
+ url.searchParams.set("client_id", input.oauth.clientId);
4320
+ url.searchParams.set("redirect_uri", input.redirectUri);
4321
+ url.searchParams.set("code_challenge", input.codeChallenge);
4322
+ url.searchParams.set("code_challenge_method", "S256");
4323
+ url.searchParams.set("state", input.state);
4324
+ if (input.scopes.length > 0) {
4325
+ url.searchParams.set("scope", input.scopes.join(" "));
4326
+ }
4327
+ return url.toString();
4328
+ }
4329
+ async function exchangeAuthorizationCode(input) {
4330
+ const body = new URLSearchParams({
4331
+ grant_type: "authorization_code",
4332
+ code: input.code,
4333
+ redirect_uri: input.redirectUri,
4334
+ client_id: input.oauth.clientId,
4335
+ code_verifier: input.codeVerifier
4336
+ });
4337
+ const tokens = await postForm(input.fetch, input.oauth.tokenUrl, body);
4338
+ return toTokenSet(tokens);
4339
+ }
4340
+ async function postForm(fetchImpl, url, body) {
4341
+ const response = await fetchImpl(url, {
4342
+ method: "POST",
4343
+ headers: {
4344
+ "Content-Type": "application/x-www-form-urlencoded",
4345
+ Accept: "application/json"
4346
+ },
4347
+ body: body.toString()
4348
+ });
4349
+ const text = await response.text();
4350
+ const parsed = text.length === 0 ? {} : JSON.parse(text);
4351
+ if (!response.ok) {
4352
+ throw new Error(oauthTokenError(parsed, response.status));
4353
+ }
4354
+ if (typeof parsed !== "object" || parsed === null) {
4355
+ throw new Error("OAuth token endpoint returned a non-object response.");
4356
+ }
4357
+ return parsed;
4358
+ }
4359
+ function toTokenSet(tokens) {
4360
+ const accessToken = tokens.access_token;
4361
+ if (typeof accessToken !== "string" || accessToken.trim().length === 0) {
4362
+ throw new Error("OAuth token endpoint did not return an access token.");
4363
+ }
4364
+ const set = { accessToken, tokenLabel: oauthTokenLabel() };
4365
+ const refreshToken = tokens.refresh_token;
4366
+ if (typeof refreshToken === "string" && refreshToken.trim().length > 0) {
4367
+ set.refreshToken = refreshToken;
4368
+ }
4369
+ const expiresAt = expiresAtFrom(tokens.expires_in);
4370
+ if (expiresAt !== void 0) {
4371
+ set.expiresAt = expiresAt;
4372
+ }
4373
+ return set;
4374
+ }
4375
+ function expiresAtFrom(expiresIn) {
4376
+ if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
4377
+ return void 0;
4378
+ }
4379
+ return new Date(Date.now() + expiresIn * 1e3).toISOString();
4380
+ }
4381
+ function mergeScopes(derived, defaults) {
4382
+ const merged = new Set(derived);
4383
+ for (const scope of defaults ?? []) {
4384
+ merged.add(scope);
4385
+ }
4386
+ return [...merged];
4387
+ }
4388
+ function oauthTokenLabel() {
4389
+ const name = hostname().trim();
4390
+ return name.length === 0 ? "oauth" : `oauth-${name}`;
4391
+ }
4392
+ function farFuture() {
4393
+ return new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3).toISOString();
4394
+ }
4395
+ async function tryOpenBrowser(openBrowser, url) {
4396
+ if (openBrowser === void 0) return;
4397
+ await openBrowser(url).catch(() => void 0);
4398
+ }
4399
+ function oauthTokenError(parsed, status2) {
4400
+ if (typeof parsed === "object" && parsed !== null) {
4401
+ const record = parsed;
4402
+ const error = typeof record.error === "string" ? record.error : void 0;
4403
+ const description = typeof record.error_description === "string" ? record.error_description : void 0;
4404
+ if (error !== void 0) {
4405
+ return description === void 0 ? `OAuth token request failed: ${error}.` : `OAuth token request failed: ${error}: ${description}`;
4406
+ }
4407
+ }
4408
+ return `OAuth token request failed with status ${status2}.`;
4409
+ }
4410
+
4411
+ // src/auth/serviceTokenLogin.ts
4412
+ import { hostname as hostname2 } from "os";
4413
+ async function serviceTokenLogin(input) {
4414
+ await input.credentialStore.assertAvailable();
4415
+ const callerBearer = await input.resolveCallerBearer();
4416
+ if (callerBearer === void 0) {
4417
+ throw new Error(
4418
+ "Headless login needs an authenticated caller. Set SIFT_API_TOKEN or run 'sift login' once interactively, then retry 'sift login --no-browser'."
4419
+ );
4420
+ }
4421
+ const options = parseOptions(input.rest);
4422
+ const requestBody = buildServiceTokenRequest({
4423
+ rest: input.rest,
4424
+ capabilities: input.capabilities,
4425
+ label: options.get("label"),
4426
+ workspaceId: options.get("workspace-id"),
4427
+ ttlDays: options.get("ttl-days")
4428
+ });
4429
+ const minted = await postServiceTokenMint(
4430
+ input.fetch,
4431
+ `${input.apiBaseUrl}/cli-auth/service-token`,
4432
+ callerBearer,
4433
+ requestBody
4434
+ );
4435
+ const profile = {
4436
+ apiBaseUrl: input.apiBaseUrl,
4437
+ appBaseUrl: input.appBaseUrl,
4438
+ workspaceId: minted.workspaceId,
4439
+ brainId: minted.brainId,
4440
+ principalId: minted.principalId,
4441
+ tokenId: minted.tokenId,
4442
+ tokenLabel: minted.tokenLabel,
4443
+ tokenExpiresAt: minted.tokenExpiresAt,
4444
+ capabilities: minted.capabilities,
4445
+ tokenKind: "service"
4446
+ };
4447
+ return { profile, token: minted.token };
4448
+ }
4449
+ function buildServiceTokenRequest(input) {
4450
+ const body = {
4451
+ label: clean4(input.label) ?? defaultLabel()
4452
+ };
4453
+ const workspaceId = clean4(input.workspaceId);
4454
+ if (workspaceId !== void 0) {
4455
+ body.workspaceId = workspaceId;
4456
+ }
4457
+ if (capabilityFlagPresent(input.rest)) {
4458
+ body.capabilities = input.capabilities;
4459
+ }
4460
+ const ttlDays = clean4(input.ttlDays);
4461
+ if (ttlDays !== void 0) {
4462
+ const parsed = Number(ttlDays);
4463
+ if (!Number.isInteger(parsed) || parsed <= 0) {
4464
+ throw new Error("Option --ttl-days must be a positive integer.");
4465
+ }
4466
+ body.ttlDays = parsed;
4467
+ }
4468
+ return body;
4469
+ }
4470
+ function capabilityFlagPresent(rest) {
4471
+ return rest.includes("--capability");
4472
+ }
4473
+ async function postServiceTokenMint(fetchImpl, url, callerBearer, body) {
4474
+ const response = await fetchImpl(url, {
4475
+ method: "POST",
4476
+ headers: {
4477
+ "Content-Type": "application/json",
4478
+ Authorization: `Bearer ${callerBearer}`
4479
+ },
4480
+ body: JSON.stringify(body)
4481
+ });
4482
+ const text = await response.text();
4483
+ const parsed = text.length === 0 ? {} : JSON.parse(text);
4484
+ if (!response.ok) {
4485
+ throw new Error(serviceTokenError(parsed, response.status));
4486
+ }
4487
+ return assertServiceTokenResponse(parsed);
4488
+ }
4489
+ function assertServiceTokenResponse(parsed) {
4490
+ if (typeof parsed !== "object" || parsed === null) {
4491
+ throw new Error("Service-token mint returned a non-object response.");
4492
+ }
4493
+ const record = parsed;
4494
+ return {
4495
+ token: requiredString(record.token, "token"),
4496
+ tokenId: requiredString(record.tokenId, "tokenId"),
4497
+ tokenLabel: requiredString(record.tokenLabel, "tokenLabel"),
4498
+ tokenExpiresAt: requiredString(record.tokenExpiresAt, "tokenExpiresAt"),
4499
+ workspaceId: requiredString(record.workspaceId, "workspaceId"),
4500
+ brainId: requiredString(record.brainId, "brainId"),
4501
+ principalId: requiredString(record.principalId, "principalId"),
4502
+ capabilities: stringArray2(record.capabilities, "capabilities")
4503
+ };
4504
+ }
4505
+ function requiredString(value, name) {
4506
+ if (typeof value !== "string" || value.trim().length === 0) {
4507
+ throw new Error(`Service-token mint response missing ${name}.`);
4508
+ }
4509
+ return value;
4510
+ }
4511
+ function stringArray2(value, name) {
4512
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
4513
+ throw new Error(`Service-token mint response field ${name} must be a string array.`);
4514
+ }
4515
+ return [...value];
4516
+ }
4517
+ function serviceTokenError(parsed, status2) {
4518
+ if (typeof parsed === "object" && parsed !== null && "error" in parsed) {
4519
+ const error = parsed.error;
4520
+ if (typeof error === "object" && error !== null && "message" in error) {
4521
+ const message = error.message;
4522
+ if (typeof message === "string" && message.trim().length > 0) {
4523
+ return message;
4524
+ }
4525
+ }
4526
+ }
4527
+ return `Service-token mint failed with status ${status2}.`;
4528
+ }
4529
+ function defaultLabel() {
4530
+ const name = hostname2().trim();
4531
+ return name.length === 0 ? "sift-cli-service" : `sift-cli-service-${name}`;
4532
+ }
4533
+ function clean4(value) {
4534
+ const trimmed = value?.trim();
4535
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
4536
+ }
4537
+
4538
+ // src/auth/convergedLogin.ts
4539
+ function oauthRefresherFor(input, rest) {
4540
+ const oauth = input.oauthConfig ?? resolveCliOAuthConfig({ argv: rest, env: input.env });
4541
+ if (oauth === void 0) return void 0;
4542
+ return ({ refreshToken }) => oauthRefresh({ oauth, fetch: input.fetch, refreshToken });
4543
+ }
4544
+ async function oauthBrowserLoginFlow(input, rest, json) {
4545
+ const apiBaseUrl = await resolveLoginApiBaseUrl({ argv: rest, env: input.env, homeDir: input.homeDir });
4546
+ const oauth = resolveOAuthConfigOrThrow(input, rest);
4547
+ const result = await oauthBrowserLogin({
4548
+ apiBaseUrl,
4549
+ appBaseUrl: resolveAppBaseUrl(input.env, apiBaseUrl),
4550
+ oauth,
4551
+ capabilities: requestedCapabilities(rest),
4552
+ fetch: input.fetch,
4553
+ credentialStore: input.credentialStore,
4554
+ ...input.openBrowser === void 0 ? {} : { openBrowser: input.openBrowser },
4555
+ ...input.createCallbackServer === void 0 ? {} : { createCallbackServer: input.createCallbackServer },
4556
+ resolveScope: input.resolveScope ?? whoamiResolveScope,
4557
+ ...input.nextSecret === void 0 ? {} : { nextSecret: input.nextSecret }
4558
+ });
4559
+ return persistConvergedLogin(
4560
+ input,
4561
+ {
4562
+ profile: result.profile,
4563
+ accessToken: result.accessToken,
4564
+ ...result.refreshToken === void 0 ? {} : { refreshToken: result.refreshToken }
4565
+ },
4566
+ json
4567
+ );
4568
+ }
4569
+ async function serviceTokenLoginFlow(input, rest, json) {
4570
+ const apiBaseUrl = await resolveLoginApiBaseUrl({ argv: rest, env: input.env, homeDir: input.homeDir });
4571
+ const result = await serviceTokenLogin({
4572
+ apiBaseUrl,
4573
+ appBaseUrl: resolveAppBaseUrl(input.env, apiBaseUrl),
4574
+ rest,
4575
+ capabilities: requestedCapabilities(rest),
4576
+ fetch: input.fetch,
4577
+ credentialStore: input.credentialStore,
4578
+ resolveCallerBearer: defaultCallerBearerResolver(input)
4579
+ });
4580
+ return persistConvergedLogin(input, { profile: result.profile, accessToken: result.token }, json);
4581
+ }
4582
+ function resolveOAuthConfigOrThrow(input, rest) {
4583
+ const oauth = input.oauthConfig ?? resolveCliOAuthConfig({ argv: rest, env: input.env });
4584
+ if (oauth === void 0) {
4585
+ throw new Error(
4586
+ "OAuth login is not yet enabled. Set SIFT_OAUTH_AUTHORIZE_URL, SIFT_OAUTH_TOKEN_URL, and SIFT_OAUTH_CLIENT_ID, or omit --oauth to use the default sign-in."
4587
+ );
4588
+ }
4589
+ return oauth;
4590
+ }
4591
+ function defaultCallerBearerResolver(input) {
4592
+ return async () => {
4593
+ const envToken = clean2(input.env.SIFT_API_TOKEN);
4594
+ if (envToken !== void 0) return envToken;
4595
+ const stored = await readStoredSiftConfig({ homeDir: input.homeDir });
4596
+ const profile = stored?.profiles[stored.currentProfile];
4597
+ if (profile === void 0) return void 0;
4598
+ return input.credentialStore.read({
4599
+ apiBaseUrl: profile.apiBaseUrl,
4600
+ tokenId: profile.tokenId
4601
+ });
4602
+ };
4603
+ }
4604
+ var whoamiResolveScope = async ({ apiBaseUrl, token, fetch: fetchImpl }) => {
4605
+ const response = await fetchImpl(`${apiBaseUrl}/agent-tools/whoami`, {
4606
+ method: "POST",
4607
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
4608
+ body: JSON.stringify({ input: {} })
4609
+ });
4610
+ const text = await response.text();
4611
+ const parsed = text.length === 0 ? {} : JSON.parse(text);
4612
+ if (!response.ok) {
4613
+ throw new Error(errorMessage2(parsed, response.status));
4614
+ }
4615
+ return whoamiScopeFrom(parsed);
4616
+ };
4617
+ function whoamiScopeFrom(parsed) {
4618
+ if (typeof parsed !== "object" || parsed === null) {
4619
+ throw new Error("whoami returned a non-object response.");
4620
+ }
4621
+ const record = parsed;
4622
+ const principalId = nestedString(record.principal, "id");
4623
+ const workspaceId = nestedString(record.scope, "workspaceId");
4624
+ const brainId = nestedString(record.scope, "brainId");
4625
+ if (principalId === void 0 || workspaceId === void 0 || brainId === void 0) {
4626
+ throw new Error("whoami response is missing principal or scope fields.");
4627
+ }
4628
+ const capabilities = Array.isArray(record.capabilities) ? record.capabilities.filter((item) => typeof item === "string") : [];
4629
+ return { principalId, workspaceId, brainId, capabilities };
4630
+ }
4631
+ function nestedString(parent, key) {
4632
+ if (typeof parent !== "object" || parent === null) return void 0;
4633
+ const value = parent[key];
4634
+ return typeof value === "string" && value.length > 0 ? value : void 0;
4635
+ }
4636
+ function resolveAppBaseUrl(env, apiBaseUrl) {
4637
+ const fromEnv = clean2(env.SIFT_APP_BASE_URL);
4638
+ if (fromEnv !== void 0) return normalizeUrl(fromEnv);
4639
+ return apiBaseUrl.replace(/\/\/api\./u, "//");
4640
+ }
4641
+ async function persistConvergedLogin(input, result, json) {
4642
+ const { profile } = result;
4643
+ try {
4644
+ await input.credentialStore.write({
4645
+ apiBaseUrl: profile.apiBaseUrl,
4646
+ tokenId: profile.tokenId,
4647
+ secret: result.accessToken
4648
+ });
4649
+ if (result.refreshToken !== void 0) {
4650
+ await input.credentialStore.write({
4651
+ apiBaseUrl: profile.apiBaseUrl,
4652
+ tokenId: refreshSlotTokenId(profile.tokenId),
4653
+ secret: result.refreshToken
4654
+ });
4655
+ }
4656
+ } catch (error) {
4657
+ return fail(
4658
+ `Sift CLI login storage failure: ${error instanceof Error ? error.message : "credential store write failed"}`
4659
+ );
4660
+ }
4661
+ await writeStoredSiftConfig({
4662
+ homeDir: input.homeDir,
4663
+ config: { currentProfile: "default", profiles: { default: profile } }
4664
+ });
4665
+ const scope = {
4666
+ apiBaseUrl: profile.apiBaseUrl,
4667
+ tokenLabel: profile.tokenLabel,
4668
+ tokenExpiresAt: profile.tokenExpiresAt,
4669
+ principalId: profile.principalId,
4670
+ workspaceId: profile.workspaceId,
4671
+ brainId: profile.brainId,
4672
+ capabilities: profile.capabilities
4673
+ };
4674
+ return ok(json ? `${JSON.stringify(scope)}
4675
+ ` : `Authenticated Sift CLI
4676
+ ${renderScope(scope)}`);
4677
+ }
4678
+
3454
4679
  // src/auth/loginFlow.ts
3455
4680
  var execFileAsync2 = promisify2(execFile2);
3456
4681
  function createSiftCliAuthCommands(input) {
3457
4682
  const now = input.now ?? (() => /* @__PURE__ */ new Date());
3458
- const sleep = input.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
4683
+ const sleep = input.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
3459
4684
  return {
3460
4685
  async login({ rest, json }) {
3461
4686
  try {
3462
- return rest.includes("--no-browser") ? await deviceLogin(input, rest, sleep, json) : await browserLogin(input, rest, json);
4687
+ return await routeLogin(input, rest, sleep, json);
3463
4688
  } catch (error) {
3464
4689
  return json ? failJson(error instanceof Error ? error.message : "Login failed.") : fail(error instanceof Error ? error.message : "Login failed.");
3465
4690
  }
@@ -3475,21 +4700,18 @@ function createSiftCliAuthCommands(input) {
3475
4700
  env: input.env,
3476
4701
  homeDir: input.homeDir,
3477
4702
  credentialStore: input.credentialStore,
3478
- now: now()
4703
+ now: now(),
4704
+ oauthRefresher: oauthRefresherFor(input, [])
3479
4705
  });
3480
4706
  }
3481
4707
  };
3482
4708
  }
3483
- async function resolveLoginApiBaseUrl(input) {
3484
- const options = parseOptions(input.argv);
3485
- const fromFlag = clean2(options.get("api-base-url"));
3486
- if (fromFlag !== void 0) return normalizeUrl(fromFlag);
3487
- const fromEnv = clean2(input.env.SIFT_API_BASE_URL);
3488
- if (fromEnv !== void 0) return normalizeUrl(fromEnv);
3489
- const stored = await readStoredSiftConfig({ homeDir: input.homeDir });
3490
- const profile = stored?.profiles[stored.currentProfile];
3491
- if (profile !== void 0) return normalizeUrl(profile.apiBaseUrl);
3492
- return "https://api.sift.com";
4709
+ async function routeLogin(input, rest, sleep, json) {
4710
+ const noBrowser = rest.includes("--no-browser");
4711
+ if (oauthLoginSelected({ argv: rest, env: input.env })) {
4712
+ return noBrowser ? serviceTokenLoginFlow(input, rest, json) : oauthBrowserLoginFlow(input, rest, json);
4713
+ }
4714
+ return noBrowser ? deviceLogin(input, rest, sleep, json) : browserLogin(input, rest, json);
3493
4715
  }
3494
4716
  async function browserLogin(input, rest, json) {
3495
4717
  await input.credentialStore.assertAvailable();
@@ -3503,10 +4725,10 @@ async function browserLogin(input, rest, json) {
3503
4725
  codeChallenge: pkce.codeChallenge,
3504
4726
  codeChallengeMethod: "S256",
3505
4727
  stateHash: pkce.stateHash,
3506
- deviceLabel: input.deviceLabel ?? hostname(),
4728
+ deviceLabel: input.deviceLabel ?? hostname3(),
3507
4729
  requestedCapabilities: requestedCapabilities(rest)
3508
4730
  });
3509
- await tryOpenBrowser(input.openBrowser, request.authorizeUrl);
4731
+ await tryOpenBrowser2(input.openBrowser, request.authorizeUrl);
3510
4732
  const callback = await callbackServer.waitForCallback();
3511
4733
  if (callback.state !== pkce.stateHash) {
3512
4734
  throw new Error("CLI auth callback state mismatch.");
@@ -3526,7 +4748,7 @@ async function deviceLogin(input, rest, sleep, json) {
3526
4748
  await input.credentialStore.assertAvailable();
3527
4749
  const apiBaseUrl = await resolveLoginApiBaseUrl({ argv: rest, env: input.env, homeDir: input.homeDir });
3528
4750
  const request = await postJson(input.fetch, `${apiBaseUrl}/cli-auth/device`, {
3529
- deviceLabel: input.deviceLabel ?? hostname(),
4751
+ deviceLabel: input.deviceLabel ?? hostname3(),
3530
4752
  requestedCapabilities: requestedCapabilities(rest)
3531
4753
  });
3532
4754
  let intervalSeconds = request.intervalSeconds;
@@ -3538,9 +4760,9 @@ async function deviceLogin(input, rest, sleep, json) {
3538
4760
  { requestId: request.requestId, userCode: request.userCode }
3539
4761
  );
3540
4762
  if ("token" in token) {
3541
- const result2 = await persistLogin(input, token, json);
3542
- return result2.exitCode === 0 ? { ...result2, stdout: `Code: ${request.userCode}
3543
- ${result2.stdout}` } : result2;
4763
+ const result = await persistLogin(input, token, json);
4764
+ return result.exitCode === 0 ? { ...result, stdout: `Code: ${request.userCode}
4765
+ ${result.stdout}` } : result;
3544
4766
  }
3545
4767
  if (token.status === "authorization_pending" || token.status === "slow_down") {
3546
4768
  intervalSeconds = token.intervalSeconds;
@@ -3564,8 +4786,8 @@ async function persistLogin(input, token, json) {
3564
4786
  `Sift CLI login storage failure: ${error instanceof Error ? error.message : "credential store write failed"}`
3565
4787
  );
3566
4788
  }
3567
- const config2 = configFromToken(token);
3568
- await writeStoredSiftConfig({ homeDir: input.homeDir, config: config2 });
4789
+ const config = configFromToken(token);
4790
+ await writeStoredSiftConfig({ homeDir: input.homeDir, config });
3569
4791
  if (oldProfile !== void 0) {
3570
4792
  const oldSecret = await input.credentialStore.read({
3571
4793
  apiBaseUrl: oldProfile.apiBaseUrl,
@@ -3604,6 +4826,33 @@ async function authStatus(input, now, json) {
3604
4826
  if (profile === void 0) {
3605
4827
  return ok(json ? '{"auth":"none"}\n' : "Auth: none\n");
3606
4828
  }
4829
+ const expired = Date.parse(profile.tokenExpiresAt) <= now.getTime();
4830
+ if (expired) {
4831
+ return staleStoredStatus(
4832
+ profile,
4833
+ "expired",
4834
+ "Stored Sift CLI auth has expired; run `sift login` again.",
4835
+ json
4836
+ );
4837
+ }
4838
+ let secret;
4839
+ try {
4840
+ secret = await input.credentialStore.read({
4841
+ apiBaseUrl: profile.apiBaseUrl,
4842
+ tokenId: profile.tokenId
4843
+ });
4844
+ } catch (error) {
4845
+ const message = error instanceof Error ? error.message : "Stored Sift credential store could not be read; run `sift login` again.";
4846
+ return staleStoredStatus(profile, "credential_store_unavailable", message, json);
4847
+ }
4848
+ if (secret === void 0) {
4849
+ return staleStoredStatus(
4850
+ profile,
4851
+ "credential_missing",
4852
+ "Stored Sift credential store secret is missing; run `sift login` again.",
4853
+ json
4854
+ );
4855
+ }
3607
4856
  return ok(
3608
4857
  json ? `${JSON.stringify({ auth: "stored", ...profile })}
3609
4858
  ` : [
@@ -3618,6 +4867,24 @@ async function authStatus(input, now, json) {
3618
4867
  ].join("\n")
3619
4868
  );
3620
4869
  }
4870
+ function staleStoredStatus(profile, reason, message, json) {
4871
+ if (json) {
4872
+ return ok(
4873
+ `${JSON.stringify({ auth: "stored", status: "stale", reason, message, ...profile })}
4874
+ `
4875
+ );
4876
+ }
4877
+ return ok(
4878
+ [
4879
+ "Auth: stale",
4880
+ `Reason: ${reason}`,
4881
+ message,
4882
+ `API: ${profile.apiBaseUrl}`,
4883
+ `Token: ${profile.tokenLabel}`,
4884
+ ""
4885
+ ].join("\n")
4886
+ );
4887
+ }
3621
4888
  async function logout(input, now, json) {
3622
4889
  if (clean2(input.env.SIFT_API_TOKEN) !== void 0) {
3623
4890
  return ok(json ? '{"status":"env_auth_active"}\n' : "Auth: env\nUnset SIFT_API_TOKEN to log out.\n");
@@ -3652,10 +4919,6 @@ function okStatus(source, loaded, json) {
3652
4919
  ${renderScope(loaded.config)}`
3653
4920
  );
3654
4921
  }
3655
- function requestedCapabilities(rest) {
3656
- const option = parseOptions(rest).get("capability");
3657
- return option === void 0 ? ["record:read"] : option.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
3658
- }
3659
4922
  function configFromToken(token) {
3660
4923
  return {
3661
4924
  currentProfile: "default",
@@ -3674,7 +4937,7 @@ function configFromToken(token) {
3674
4937
  }
3675
4938
  };
3676
4939
  }
3677
- async function tryOpenBrowser(openBrowser, url) {
4940
+ async function tryOpenBrowser2(openBrowser, url) {
3678
4941
  await (openBrowser ?? openBrowserUrl)(url).catch(() => void 0);
3679
4942
  }
3680
4943
  async function openBrowserUrl(url) {
@@ -3704,16 +4967,6 @@ async function postJson(fetchImpl, url, body, headers = {}) {
3704
4967
  }
3705
4968
  return parsed;
3706
4969
  }
3707
- function errorMessage2(parsed, status2) {
3708
- if (typeof parsed === "object" && parsed !== null && "error" in parsed) {
3709
- const error = parsed.error;
3710
- if (typeof error === "object" && error !== null && "message" in error) {
3711
- const message = error.message;
3712
- if (typeof message === "string") return message;
3713
- }
3714
- }
3715
- return `CLI auth request failed with status ${status2}.`;
3716
- }
3717
4970
  function failJson(message) {
3718
4971
  return {
3719
4972
  exitCode: 1,
@@ -3722,12 +4975,243 @@ function failJson(message) {
3722
4975
  stderr: ""
3723
4976
  };
3724
4977
  }
3725
- function normalizeUrl(value) {
3726
- return value.replace(/\/+$/u, "");
4978
+
4979
+ // src/roamMcpReader.ts
4980
+ import { spawn } from "child_process";
4981
+ function createRoamMcpReader(input = {}) {
4982
+ return {
4983
+ async exportPages(request) {
4984
+ const client = createRoamMcpJsonLineClient({
4985
+ command: input.command ?? "npx",
4986
+ args: input.args ?? ["-y", "@roam-research/roam-mcp"],
4987
+ spawnProcess: input.spawnProcess ?? spawn
4988
+ });
4989
+ try {
4990
+ return await exportRoamPagesFromMcp(client, request);
4991
+ } finally {
4992
+ await client.close();
4993
+ }
4994
+ }
4995
+ };
3727
4996
  }
3728
- function clean2(value) {
3729
- const trimmed = value?.trim();
3730
- return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
4997
+ async function exportRoamPagesFromMcp(client, input) {
4998
+ const graphId = await resolveGraphId(client, input.graph);
4999
+ await client.callTool("get_graph_guidelines", graphArgs(graphId)).catch(() => void 0);
5000
+ const tuples = input.scope === "sift_tag" ? await queryMarkedPageTuples(client, graphId, input.limit) : await queryWholeGraphPageTuples(client, graphId, input.limit);
5001
+ const records = [];
5002
+ const seen = /* @__PURE__ */ new Set();
5003
+ for (const tuple of tuples) {
5004
+ const key = tuple.uid ?? tuple.title;
5005
+ if (seen.has(key)) continue;
5006
+ seen.add(key);
5007
+ const markdown = await fetchPageMarkdown(client, graphId, tuple);
5008
+ records.push({
5009
+ graphId,
5010
+ pageUid: tuple.uid ?? stableFallbackUid(tuple.title),
5011
+ pageTitle: tuple.title,
5012
+ markdown,
5013
+ scope: input.scope,
5014
+ blockCount: estimateBlockCount(markdown),
5015
+ importedAt: input.now.toISOString()
5016
+ });
5017
+ }
5018
+ return records;
5019
+ }
5020
+ async function resolveGraphId(client, graph) {
5021
+ if (graph !== void 0) return graph;
5022
+ const result = await client.callTool("list_graphs", {});
5023
+ const parsed = parseToolJson(result);
5024
+ const graphId = firstGraphId(parsed);
5025
+ if (graphId !== void 0) return graphId;
5026
+ const message = errorMessageFromParsedTool(parsed);
5027
+ if (message !== void 0) throw new Error(message);
5028
+ return "local-graph";
5029
+ }
5030
+ async function queryMarkedPageTuples(client, graphId, limit) {
5031
+ const result = await client.callTool("datalog_query", {
5032
+ ...graphArgs(graphId),
5033
+ query: '[:find ?title ?uid :where [?tag :node/title "Sift"] [?block :block/refs ?tag] [?block :block/page ?page] [?page :node/title ?title] [?page :block/uid ?uid]]'
5034
+ });
5035
+ return tuplesFromToolResult(result).slice(0, limit);
5036
+ }
5037
+ async function queryWholeGraphPageTuples(client, graphId, limit) {
5038
+ const result = await client.callTool("datalog_query", {
5039
+ ...graphArgs(graphId),
5040
+ query: "[:find ?title ?uid :where [?page :node/title ?title] [?page :block/uid ?uid]]"
5041
+ });
5042
+ return tuplesFromToolResult(result).slice(0, limit);
5043
+ }
5044
+ async function fetchPageMarkdown(client, graphId, tuple) {
5045
+ const result = await client.callTool("get_page", {
5046
+ ...graphArgs(graphId),
5047
+ ...tuple.uid === void 0 ? { title: tuple.title } : { uid: tuple.uid }
5048
+ });
5049
+ const text = toolText(result);
5050
+ const parsed = parseJsonIfPossible(text);
5051
+ const markdown = markdownFromParsed(parsed) ?? text;
5052
+ return stripRoamMetadataTags(markdown).trim();
5053
+ }
5054
+ function graphArgs(graphId) {
5055
+ return { graph: graphId };
5056
+ }
5057
+ function tuplesFromToolResult(result) {
5058
+ const parsed = parseToolJson(result);
5059
+ const values = candidateArrays(parsed);
5060
+ const tuples = [];
5061
+ for (const value of values) {
5062
+ if (Array.isArray(value) && typeof value[0] === "string") {
5063
+ tuples.push({ title: value[0], ...typeof value[1] === "string" ? { uid: value[1] } : {} });
5064
+ } else if (isRecord(value)) {
5065
+ const title = stringProperty(value, ["title", "pageTitle", "name"]);
5066
+ const uid = stringProperty(value, ["uid", "pageUid"]);
5067
+ if (title !== void 0) tuples.push({ title, ...uid === void 0 ? {} : { uid } });
5068
+ }
5069
+ }
5070
+ return tuples;
5071
+ }
5072
+ function parseToolJson(result) {
5073
+ const text = toolText(result);
5074
+ return parseJsonIfPossible(text);
5075
+ }
5076
+ function toolText(result) {
5077
+ if (isRecord(result) && Array.isArray(result.content)) {
5078
+ return result.content.flatMap((item) => isRecord(item) && typeof item.text === "string" ? [item.text] : []).join("\n").trim();
5079
+ }
5080
+ return typeof result === "string" ? result : JSON.stringify(result);
5081
+ }
5082
+ function parseJsonIfPossible(text) {
5083
+ try {
5084
+ return JSON.parse(text);
5085
+ } catch {
5086
+ return text;
5087
+ }
5088
+ }
5089
+ function candidateArrays(value) {
5090
+ if (Array.isArray(value)) return value;
5091
+ if (!isRecord(value)) return [];
5092
+ for (const key of ["results", "result", "data", "rows", "pages"]) {
5093
+ const candidate = value[key];
5094
+ if (Array.isArray(candidate)) return candidate;
5095
+ }
5096
+ return [];
5097
+ }
5098
+ function markdownFromParsed(value) {
5099
+ if (typeof value === "string") return value;
5100
+ if (!isRecord(value)) return void 0;
5101
+ return stringProperty(value, ["markdown", "content", "text"]);
5102
+ }
5103
+ function firstGraphId(value) {
5104
+ const graphs = isRecord(value) && Array.isArray(value.graphs) ? value.graphs : candidateArrays(value);
5105
+ for (const graph of graphs) {
5106
+ if (!isRecord(graph)) continue;
5107
+ const id = stringProperty(graph, ["nickname", "graph", "name", "id"]);
5108
+ if (id !== void 0) return id;
5109
+ }
5110
+ return void 0;
5111
+ }
5112
+ function errorMessageFromParsedTool(value) {
5113
+ if (!isRecord(value) || !isRecord(value.error)) return void 0;
5114
+ const message = value.error.message;
5115
+ return typeof message === "string" ? message : void 0;
5116
+ }
5117
+ function stringProperty(record, keys) {
5118
+ for (const key of keys) {
5119
+ const value = record[key];
5120
+ if (typeof value === "string" && value.trim().length > 0) return value;
5121
+ }
5122
+ return void 0;
5123
+ }
5124
+ function stripRoamMetadataTags(markdown) {
5125
+ return markdown.replace(/<roam\b[^>]*>/giu, "").replace(/<\/roam>/giu, "");
5126
+ }
5127
+ function estimateBlockCount(markdown) {
5128
+ const lines = markdown.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
5129
+ return Math.max(1, lines.length);
5130
+ }
5131
+ function stableFallbackUid(title) {
5132
+ return title.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 80);
5133
+ }
5134
+ function isRecord(value) {
5135
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5136
+ }
5137
+ function createRoamMcpJsonLineClient(input) {
5138
+ const child = input.spawnProcess(input.command, input.args, {
5139
+ stdio: ["pipe", "pipe", "pipe"]
5140
+ });
5141
+ child.stderr.resume();
5142
+ child.stdout.setEncoding("utf8");
5143
+ let nextId = 1;
5144
+ let buffer = "";
5145
+ const pending = /* @__PURE__ */ new Map();
5146
+ child.stdout.on("data", (chunk) => {
5147
+ buffer += chunk;
5148
+ let newline = buffer.indexOf("\n");
5149
+ while (newline >= 0) {
5150
+ const line = buffer.slice(0, newline).trim();
5151
+ buffer = buffer.slice(newline + 1);
5152
+ if (line.length > 0) handleJsonRpcLine(line, pending);
5153
+ newline = buffer.indexOf("\n");
5154
+ }
5155
+ });
5156
+ child.on("error", (error) => {
5157
+ for (const entry of pending.values()) entry.reject(error);
5158
+ pending.clear();
5159
+ });
5160
+ child.on("exit", () => {
5161
+ for (const entry of pending.values()) entry.reject(new Error("Roam MCP server exited."));
5162
+ pending.clear();
5163
+ });
5164
+ const request = (method, params) => {
5165
+ const id = nextId;
5166
+ nextId += 1;
5167
+ const promise = new Promise((resolve2, reject) => {
5168
+ pending.set(id, { resolve: resolve2, reject });
5169
+ });
5170
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}
5171
+ `);
5172
+ return promise;
5173
+ };
5174
+ const initialized = request("initialize", {
5175
+ protocolVersion: "2025-11-25",
5176
+ capabilities: {},
5177
+ clientInfo: { name: "sift-roam-import", version: "0.1.0" }
5178
+ }).then(() => {
5179
+ child.stdin.write(
5180
+ `${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}
5181
+ `
5182
+ );
5183
+ });
5184
+ return {
5185
+ async callTool(name, args) {
5186
+ await initialized;
5187
+ return request("tools/call", { name, arguments: args });
5188
+ },
5189
+ async close() {
5190
+ child.stdin.end();
5191
+ child.kill("SIGTERM");
5192
+ }
5193
+ };
5194
+ }
5195
+ function handleJsonRpcLine(line, pending) {
5196
+ let parsed;
5197
+ try {
5198
+ parsed = JSON.parse(line);
5199
+ } catch {
5200
+ return;
5201
+ }
5202
+ if (typeof parsed.id !== "number") return;
5203
+ const entry = pending.get(parsed.id);
5204
+ if (entry === void 0) return;
5205
+ pending.delete(parsed.id);
5206
+ if (parsed.error !== void 0) {
5207
+ entry.reject(
5208
+ new Error(
5209
+ typeof parsed.error.message === "string" ? parsed.error.message : "Roam MCP error."
5210
+ )
5211
+ );
5212
+ return;
5213
+ }
5214
+ entry.resolve(parsed.result);
3731
5215
  }
3732
5216
 
3733
5217
  // src/bin/sift.ts
@@ -3738,49 +5222,92 @@ var authCommands = createSiftCliAuthCommands({
3738
5222
  credentialStore,
3739
5223
  fetch
3740
5224
  });
3741
- var loadedAuth = await authCommands.loadAuth();
3742
- var config = loadedAuth?.config ?? {
3743
- apiBaseUrl: "",
3744
- tokenLabel: "unset",
3745
- workspaceId: "",
3746
- brainId: "",
3747
- principalId: "",
3748
- capabilities: []
3749
- };
3750
5225
  var { argv, agentName } = extractAgentName(process.argv.slice(2), process.env.SIFT_AGENT);
3751
- var result = await runSiftCli({
3752
- argv,
3753
- config,
3754
- readStdin,
3755
- agentName,
3756
- executor: loadedAuth === void 0 ? void 0 : createHostedApiExecutor({
3757
- apiBaseUrl: loadedAuth.config.apiBaseUrl,
3758
- token: loadedAuth.token,
3759
- workspaceId: loadedAuth.config.workspaceId,
3760
- brainId: loadedAuth.config.brainId,
3761
- agentName
3762
- }),
3763
- authCommands,
3764
- mcpServer: {
3765
- serve: async ({ config: config2, executor }) => {
3766
- if (executor === void 0) {
3767
- throw new Error("No Sift API executor is configured for mcp.serve.");
5226
+ var startupAuth = await loadAuthForStartup(authCommands, argv);
5227
+ if (startupAuth.ok === false) {
5228
+ process.stderr.write(`${startupAuth.message}
5229
+ `);
5230
+ process.exitCode = 1;
5231
+ } else {
5232
+ const loadedAuth = startupAuth.loadedAuth;
5233
+ const config = loadedAuth?.config ?? {
5234
+ apiBaseUrl: "",
5235
+ tokenLabel: "unset",
5236
+ workspaceId: "",
5237
+ brainId: "",
5238
+ principalId: "",
5239
+ capabilities: []
5240
+ };
5241
+ const result = await runSiftCli({
5242
+ argv,
5243
+ config,
5244
+ readStdin,
5245
+ agentName,
5246
+ executor: loadedAuth === void 0 ? void 0 : createHostedApiExecutor({
5247
+ apiBaseUrl: loadedAuth.config.apiBaseUrl,
5248
+ token: loadedAuth.token,
5249
+ workspaceId: loadedAuth.config.workspaceId,
5250
+ brainId: loadedAuth.config.brainId,
5251
+ agentName
5252
+ }),
5253
+ roamReader: createRoamMcpReader(),
5254
+ roamImporter: loadedAuth === void 0 ? void 0 : createSiftRoamImportClient({
5255
+ apiBaseUrl: loadedAuth.config.apiBaseUrl,
5256
+ token: loadedAuth.token,
5257
+ workspaceId: loadedAuth.config.workspaceId
5258
+ }),
5259
+ authCommands,
5260
+ mcpServer: {
5261
+ serve: async ({ config: config2, executor }) => {
5262
+ if (executor === void 0) {
5263
+ throw new Error(
5264
+ "Not signed in. Run 'sift login' to authenticate, then 'sift mcp serve' to start the local MCP server."
5265
+ );
5266
+ }
5267
+ const { createLocalMcpStdioServer: createLocalMcpStdioServer2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
5268
+ return createLocalMcpStdioServer2({
5269
+ input: process.stdin,
5270
+ output: process.stdout,
5271
+ error: process.stderr
5272
+ }).serve({
5273
+ capabilities: config2.capabilities,
5274
+ executor
5275
+ });
3768
5276
  }
3769
- const { createLocalMcpStdioServer: createLocalMcpStdioServer2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
3770
- return createLocalMcpStdioServer2({
3771
- input: process.stdin,
3772
- output: process.stdout,
3773
- error: process.stderr
3774
- }).serve({
3775
- capabilities: config2.capabilities,
3776
- executor
3777
- });
3778
5277
  }
5278
+ });
5279
+ process.stdout.write(result.stdout);
5280
+ process.stderr.write(result.stderr);
5281
+ process.exitCode = result.exitCode;
5282
+ }
5283
+ async function loadAuthForStartup(commands, args) {
5284
+ try {
5285
+ const loadedAuth = await commands.loadAuth();
5286
+ if (loadedAuth === void 0 && !canRunWithoutLoadedAuth(args)) {
5287
+ return { ok: false, message: missingAuthMessage(args) };
5288
+ }
5289
+ return { ok: true, loadedAuth };
5290
+ } catch (error) {
5291
+ if (canRunWithoutLoadedAuth(args)) {
5292
+ return { ok: true, loadedAuth: void 0 };
5293
+ }
5294
+ return {
5295
+ ok: false,
5296
+ message: error instanceof Error ? error.message : "Failed to load Sift CLI auth."
5297
+ };
3779
5298
  }
3780
- });
3781
- process.stdout.write(result.stdout);
3782
- process.stderr.write(result.stderr);
3783
- process.exitCode = result.exitCode;
5299
+ }
5300
+ function missingAuthMessage(args) {
5301
+ const commandArgs = args.filter((arg) => arg !== "--json");
5302
+ const [group, command] = commandArgs;
5303
+ const capability = group === "roam" && command === "import" ? " --capability record:read,source:manage" : "";
5304
+ return `Not signed in. Run 'npx -y @sift-wiki/cli@latest login --api-base-url ${DEFAULT_SIFT_API_BASE_URL}${capability}', then retry this command.`;
5305
+ }
5306
+ function canRunWithoutLoadedAuth(args) {
5307
+ const commandArgs = args.filter((arg) => arg !== "--json");
5308
+ const [group, command] = commandArgs;
5309
+ return group === void 0 || group === "help" || group === "--help" || group === "doctor" || group === "skill" || group === "login" || group === "logout" || group === "auth" && command === "status";
5310
+ }
3784
5311
  function extractAgentName(args, envAgentName) {
3785
5312
  const flagIndex = args.indexOf("--as-agent");
3786
5313
  if (flagIndex !== -1 && args[flagIndex + 1] !== void 0) {