@sift-wiki/cli 0.1.3 → 0.1.4

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 +31 -22
  2. package/dist/bin/sift.js +1152 -199
  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
  }
@@ -375,6 +377,19 @@ function writeTool(name, summary, properties, cliExample, options) {
375
377
  hostedAgent: options?.hostedAgent
376
378
  });
377
379
  }
380
+ function hostedAgentOnlyReadTool(name, summary, properties, options) {
381
+ return defineTool({
382
+ name,
383
+ summary,
384
+ properties,
385
+ required: options.required,
386
+ capability: "record:read",
387
+ mutability: "read",
388
+ transports: [],
389
+ cliExample: "",
390
+ hostedAgent: { available: true, ...options.hostedAgent }
391
+ });
392
+ }
378
393
  function sourceWriteTool(name, summary, properties, cliExample) {
379
394
  return defineTool({
380
395
  name,
@@ -438,31 +453,8 @@ function defaultRiskClass(mutability) {
438
453
  return "low";
439
454
  }
440
455
  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
- }
456
+ const [prefix = ""] = name.split(".");
457
+ return defaultToolsetsByPrefix[prefix] ?? ["brain"];
466
458
  }
467
459
  function defaultSearchTerms(name, summary) {
468
460
  return [.../* @__PURE__ */ new Set([...tokenize(name), ...tokenize(summary)])];
@@ -473,7 +465,7 @@ function tokenize(text) {
473
465
  function stringProps(names) {
474
466
  return Object.fromEntries(names.map((name) => [name, { type: "string" }]));
475
467
  }
476
- var readTransports, writeTransports, NO_CAPABILITY, toolDefinitions;
468
+ var readTransports, writeTransports, NO_CAPABILITY, defaultToolsetsByPrefix, toolDefinitions;
477
469
  var init_registry = __esm({
478
470
  "../tools/dist/registry.js"() {
479
471
  "use strict";
@@ -483,6 +475,23 @@ var init_registry = __esm({
483
475
  readTransports = ["cli", "hosted_mcp", "local_mcp"];
484
476
  writeTransports = ["cli", "hosted_mcp", "local_mcp"];
485
477
  NO_CAPABILITY = "none";
478
+ defaultToolsetsByPrefix = {
479
+ audit: ["audit"],
480
+ capture: ["brain", "ingestion"],
481
+ context: ["brain", "retrieval"],
482
+ decision: ["work"],
483
+ event: ["audit"],
484
+ evidence: ["brain", "retrieval"],
485
+ graph: ["brain", "retrieval"],
486
+ ingestion: ["brain", "ingestion"],
487
+ record: ["brain", "ingestion"],
488
+ search: ["brain", "retrieval"],
489
+ skill: ["brain", "work"],
490
+ source: ["brain", "ingestion"],
491
+ task: ["work"],
492
+ tools: ["registry"],
493
+ web: ["web"]
494
+ };
486
495
  toolDefinitions = [
487
496
  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
497
  readTool("whoami", "Return principal, actor, scope, and capabilities.", {}, "sift whoami"),
@@ -601,16 +610,68 @@ var init_registry = __esm({
601
610
  severity: { type: "string" },
602
611
  visibility: { type: "array", items: { type: "string" } }
603
612
  }, "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.", {
613
+ readTool("search.query", "Search authorized brain context and return raw cited candidate results for exploration.", {
605
614
  query: { type: "string" },
606
615
  limit: { type: "integer", minimum: 1, maximum: 20 }
607
616
  }, "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'", {
617
+ 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.", {
618
+ query: { type: "string" },
619
+ queryIssuedAt: { type: "string" },
620
+ timezone: { type: "string" },
621
+ maxChars: { type: "integer", minimum: 1 }
622
+ }, "sift context assemble 'launch risks'", {
623
+ required: ["query"],
609
624
  hostedAgent: {
610
625
  toolsets: ["brain", "retrieval"],
611
626
  searchTerms: ["context", "cite", "answer", "evidence"]
612
627
  }
613
628
  }),
629
+ hostedAgentOnlyReadTool("web.search", "Search public web sources for current or public facts.", {
630
+ query: {
631
+ type: "string",
632
+ description: "Public web search query. Do not include private Sift brain context unless the user explicitly provided it for public lookup."
633
+ },
634
+ limit: { type: "integer", minimum: 1, maximum: 10 },
635
+ recencyDays: { type: "integer", minimum: 1, maximum: 3650 },
636
+ allowedDomains: { type: "array", items: { type: "string" } },
637
+ blockedDomains: { type: "array", items: { type: "string" } }
638
+ }, {
639
+ required: ["query"],
640
+ hostedAgent: {
641
+ toolsets: ["web"],
642
+ searchTerms: [
643
+ "web",
644
+ "search",
645
+ "current",
646
+ "public",
647
+ "company",
648
+ "product",
649
+ "docs",
650
+ "news",
651
+ "pricing",
652
+ "people",
653
+ "law",
654
+ "rules"
655
+ ],
656
+ inputHints: ["query", "limit", "recencyDays", "allowedDomains", "blockedDomains"],
657
+ riskClass: "medium"
658
+ }
659
+ }),
660
+ hostedAgentOnlyReadTool("web.fetch", "Read one selected public URL through guarded bounded extraction.", {
661
+ url: {
662
+ type: "string",
663
+ description: "Public http(s) URL to fetch. Local, private, and metadata URLs are refused."
664
+ },
665
+ maxChars: { type: "integer", minimum: 1, maximum: 12e3 }
666
+ }, {
667
+ required: ["url"],
668
+ hostedAgent: {
669
+ toolsets: ["web"],
670
+ searchTerms: ["web", "fetch", "read", "url", "page", "extract", "public"],
671
+ inputHints: ["url", "maxChars"],
672
+ riskClass: "medium"
673
+ }
674
+ }),
614
675
  readTool("context.profile", "Read a permission-filtered profile context model.", {}, "sift context profile"),
615
676
  readTool("evidence.list", "List authorized evidence links for a record.", stringProps(["recordId"]), "sift evidence list <record-id>"),
616
677
  readTool("evidence.get", "Read an authorized evidence item.", stringProps(["evidenceId"]), "sift evidence get <evidence-id>"),
@@ -713,6 +774,7 @@ var init_discovery = __esm({
713
774
  "use strict";
714
775
  init_registry();
715
776
  IMPLEMENTED_TOOL_NAMES = [
777
+ "contract.get",
716
778
  "whoami",
717
779
  "brain.list",
718
780
  "brain.use",
@@ -1034,7 +1096,9 @@ function runtimeAvailableToolNames(service) {
1034
1096
  [service.listGraphNeighbors !== void 0, ["graph.neighbors"]],
1035
1097
  [service.listEvents !== void 0, ["event.list"]],
1036
1098
  [service.getContextProfile !== void 0, ["context.profile"]],
1037
- [service.listAuditEvents !== void 0, ["audit.events"]]
1099
+ [service.listAuditEvents !== void 0, ["audit.events"]],
1100
+ [service.webSearch !== void 0, ["web.search"]],
1101
+ [service.webFetch !== void 0, ["web.fetch"]]
1038
1102
  ];
1039
1103
  return [...baseNames, ...optionalNames.flatMap(([enabled, names]) => enabled ? names : [])];
1040
1104
  }
@@ -1112,6 +1176,73 @@ var init_toolLog = __esm({
1112
1176
  }
1113
1177
  });
1114
1178
 
1179
+ // ../tools/dist/webToolRuntime.js
1180
+ function webToolHandlers(input, toolInput) {
1181
+ return {
1182
+ "web.search": () => executeWebSearch(input, toolInput),
1183
+ "web.fetch": () => executeWebFetch(input, toolInput)
1184
+ };
1185
+ }
1186
+ function executeWebSearch(input, toolInput) {
1187
+ if (input.service.webSearch === void 0) {
1188
+ throw new Error("Tool 'web.search' is unavailable without a web search service contract.");
1189
+ }
1190
+ return input.service.webSearch({ auth: input.auth, ...parseWebSearch(toolInput) });
1191
+ }
1192
+ function executeWebFetch(input, toolInput) {
1193
+ if (input.service.webFetch === void 0) {
1194
+ throw new Error("Tool 'web.fetch' is unavailable without a web fetch service contract.");
1195
+ }
1196
+ return input.service.webFetch({ auth: input.auth, ...parseWebFetch(toolInput) });
1197
+ }
1198
+ function parseWebSearch(input) {
1199
+ const parsed = {
1200
+ query: requireString(input, "query"),
1201
+ limit: requireBoundedInteger(input, "limit", 5, 1, 10)
1202
+ };
1203
+ if (input.recencyDays !== void 0) {
1204
+ parsed.recencyDays = requireBoundedInteger(input, "recencyDays", 30, 1, 3650);
1205
+ }
1206
+ if (input.allowedDomains !== void 0) {
1207
+ parsed.allowedDomains = requireBoundedStringArray(input, "allowedDomains", 10);
1208
+ }
1209
+ if (input.blockedDomains !== void 0) {
1210
+ parsed.blockedDomains = requireBoundedStringArray(input, "blockedDomains", 10);
1211
+ }
1212
+ return parsed;
1213
+ }
1214
+ function parseWebFetch(input) {
1215
+ return {
1216
+ url: requireString(input, "url"),
1217
+ maxChars: requireBoundedInteger(input, "maxChars", 8e3, 1, 12e3)
1218
+ };
1219
+ }
1220
+ function requireBoundedStringArray(input, key, maxItems) {
1221
+ const value = input[key];
1222
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
1223
+ throw new Error(`${key} must be a string array.`);
1224
+ }
1225
+ const values = value.map((item) => item.trim()).filter((item) => item.length > 0);
1226
+ if (values.length > maxItems) {
1227
+ throw new Error(`${key} must contain no more than ${maxItems} items.`);
1228
+ }
1229
+ return values;
1230
+ }
1231
+ function requireBoundedInteger(input, key, fallback, minimum, maximum) {
1232
+ const value = input[key];
1233
+ const integer = value === void 0 ? fallback : value;
1234
+ if (!Number.isInteger(integer) || Number(integer) < minimum || Number(integer) > maximum) {
1235
+ throw new Error(`${key} must be an integer between ${minimum} and ${maximum}.`);
1236
+ }
1237
+ return Number(integer);
1238
+ }
1239
+ var init_webToolRuntime = __esm({
1240
+ "../tools/dist/webToolRuntime.js"() {
1241
+ "use strict";
1242
+ init_inputParsers();
1243
+ }
1244
+ });
1245
+
1115
1246
  // ../tools/dist/executor.js
1116
1247
  function createRuntimeToolExecutor(input) {
1117
1248
  const availableToolNames = runtimeAvailableToolNames(input.service);
@@ -1179,6 +1310,7 @@ function createToolHandlers(input, toolInput) {
1179
1310
  "context.profile": () => executeContextProfile(input, toolInput),
1180
1311
  "decision.create": () => executeDecisionCreate(input, toolInput),
1181
1312
  "task.create": () => executeTaskCreate(input, toolInput),
1313
+ ...webToolHandlers(input, toolInput),
1182
1314
  ...skillToolHandlers(input, toolInput),
1183
1315
  ...agentIdentityToolHandlers(input, toolInput),
1184
1316
  ...contractToolHandlers(input),
@@ -1238,6 +1370,21 @@ function executeSearchQuery(input, toolInput) {
1238
1370
  }
1239
1371
  function executeContextAssemble(input, toolInput) {
1240
1372
  const query = parseContextQuery(toolInput);
1373
+ if (input.service.assembleGroundedContext !== void 0) {
1374
+ return input.service.assembleGroundedContext({
1375
+ auth: input.auth,
1376
+ query: query.query,
1377
+ queryIssuedAt: query.queryIssuedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1378
+ timezone: query.timezone ?? "UTC",
1379
+ requester: {
1380
+ principalId: input.auth.principalId,
1381
+ actorId: input.auth.actorId
1382
+ },
1383
+ surface: taskGuidanceSurface(input.transport),
1384
+ limit: 8,
1385
+ maxChars: query.maxChars
1386
+ });
1387
+ }
1241
1388
  return input.service.retrieveCitedContext({
1242
1389
  auth: input.auth,
1243
1390
  query: query.query,
@@ -1245,6 +1392,13 @@ function executeContextAssemble(input, toolInput) {
1245
1392
  maxChars: query.maxChars
1246
1393
  });
1247
1394
  }
1395
+ function taskGuidanceSurface(transport) {
1396
+ if (transport === "cli")
1397
+ return "cli";
1398
+ if (transport === "hosted_mcp" || transport === "local_mcp")
1399
+ return "mcp";
1400
+ return "app";
1401
+ }
1248
1402
  function executeContextProfile(input, toolInput) {
1249
1403
  if (input.service.getContextProfile === void 0) {
1250
1404
  throw new Error("Tool 'context.profile' requires a profile read service contract.");
@@ -1356,6 +1510,7 @@ var init_executor = __esm({
1356
1510
  init_toolAvailability();
1357
1511
  init_results();
1358
1512
  init_toolLog();
1513
+ init_webToolRuntime();
1359
1514
  }
1360
1515
  });
1361
1516
 
@@ -1394,7 +1549,7 @@ function createMcpAdapter(input) {
1394
1549
  ...IMPLEMENTED_TOOL_NAMES
1395
1550
  ];
1396
1551
  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));
1552
+ const available = listToolDefinitions().filter((tool) => availableNameSet.has(tool.name) && tool.transports.includes(input.transport) && isToolAuthorized(input.capabilities, tool));
1398
1553
  return {
1399
1554
  listTools() {
1400
1555
  return createMcpToolSchemas({
@@ -1546,89 +1701,41 @@ var init_hostedMcpEntrypoint = __esm({
1546
1701
  }
1547
1702
  });
1548
1703
 
1549
- // ../tools/dist/localMcpStdioServer.js
1550
- function createLocalMcpStdioServer(input) {
1704
+ // ../tools/dist/mcpJsonRpcCore.js
1705
+ function createMcpJsonRpcCore(input) {
1706
+ const { adapter, config: config2 } = input;
1551
1707
  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
- }
1708
+ async handleMessage(message) {
1709
+ if (message.id === void 0) {
1710
+ return null;
1571
1711
  }
1572
- const trailing = buffer.trim();
1573
- if (trailing.length > 0) {
1574
- await handleLine(trailing, adapter, input.output, input.error);
1712
+ const id = normalizeId(message.id);
1713
+ if (message.jsonrpc !== "2.0" || typeof message.method !== "string") {
1714
+ return errorResponse(id, -32600, "Invalid Request");
1715
+ }
1716
+ try {
1717
+ return {
1718
+ jsonrpc: "2.0",
1719
+ id,
1720
+ result: await dispatchRequest(message.method, message.params, adapter, config2)
1721
+ };
1722
+ } catch (err) {
1723
+ return errorResponse(id, -32601, err instanceof Error ? err.message : "Method not found");
1575
1724
  }
1576
1725
  }
1577
1726
  };
1578
1727
  }
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
- }
1728
+ function parseErrorResponse() {
1729
+ return errorResponse(null, -32700, "Parse error");
1623
1730
  }
1624
- async function dispatchRequest(method, params, adapter) {
1731
+ async function dispatchRequest(method, params, adapter, config2) {
1625
1732
  if (method === "initialize") {
1626
1733
  const requested = readProtocolVersion(params);
1627
1734
  return {
1628
1735
  protocolVersion: requested ?? MCP_PROTOCOL_VERSION,
1629
1736
  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."
1737
+ serverInfo: { name: config2.serverName, version: config2.version },
1738
+ instructions: config2.instructions
1632
1739
  };
1633
1740
  }
1634
1741
  if (method === "ping")
@@ -1639,7 +1746,7 @@ async function dispatchRequest(method, params, adapter) {
1639
1746
  const call = parseToolCall(params);
1640
1747
  return adapter.callTool(call);
1641
1748
  }
1642
- throw new Error(`Method '${method}' is not supported by Sift local MCP.`);
1749
+ throw new Error(`Method '${method}' is not supported by Sift MCP.`);
1643
1750
  }
1644
1751
  function readProtocolVersion(params) {
1645
1752
  if (!isRecord(params))
@@ -1655,6 +1762,9 @@ function parseToolCall(params) {
1655
1762
  arguments: isRecord(params.arguments) ? params.arguments : {}
1656
1763
  };
1657
1764
  }
1765
+ function errorResponse(id, code, message) {
1766
+ return { jsonrpc: "2.0", id, error: { code, message } };
1767
+ }
1658
1768
  function isRecord(value) {
1659
1769
  return typeof value === "object" && value !== null && !Array.isArray(value);
1660
1770
  }
@@ -1663,16 +1773,80 @@ function normalizeId(value) {
1663
1773
  return value;
1664
1774
  return null;
1665
1775
  }
1776
+ var MCP_PROTOCOL_VERSION;
1777
+ var init_mcpJsonRpcCore = __esm({
1778
+ "../tools/dist/mcpJsonRpcCore.js"() {
1779
+ "use strict";
1780
+ MCP_PROTOCOL_VERSION = "2025-11-25";
1781
+ }
1782
+ });
1783
+
1784
+ // ../tools/dist/localMcpStdioServer.js
1785
+ function createLocalMcpStdioServer(input) {
1786
+ return {
1787
+ async serve(serverInput) {
1788
+ const adapter = createMcpAdapter({
1789
+ transport: "local_mcp",
1790
+ capabilities: serverInput.capabilities,
1791
+ executor: serverInput.executor
1792
+ });
1793
+ const core = createMcpJsonRpcCore({
1794
+ adapter,
1795
+ config: {
1796
+ serverName: "sift-local-mcp",
1797
+ version: "0.1.0",
1798
+ instructions: LOCAL_INSTRUCTIONS
1799
+ }
1800
+ });
1801
+ let buffer = "";
1802
+ input.input.setEncoding("utf8");
1803
+ for await (const chunk of input.input) {
1804
+ buffer += chunk;
1805
+ let newline = buffer.indexOf("\n");
1806
+ while (newline >= 0) {
1807
+ const line = buffer.slice(0, newline).trim();
1808
+ buffer = buffer.slice(newline + 1);
1809
+ if (line.length > 0) {
1810
+ await handleLine(line, core, input.output, input.error);
1811
+ }
1812
+ newline = buffer.indexOf("\n");
1813
+ }
1814
+ }
1815
+ const trailing = buffer.trim();
1816
+ if (trailing.length > 0) {
1817
+ await handleLine(trailing, core, input.output, input.error);
1818
+ }
1819
+ }
1820
+ };
1821
+ }
1822
+ async function handleLine(line, core, output, error) {
1823
+ let message;
1824
+ try {
1825
+ message = JSON.parse(line);
1826
+ } catch {
1827
+ writeResponse(output, parseErrorResponse());
1828
+ return;
1829
+ }
1830
+ if (message.id === void 0 && message.method !== "notifications/initialized") {
1831
+ error?.write(`Ignoring MCP notification '${String(message.method)}'.
1832
+ `);
1833
+ }
1834
+ const response = await core.handleMessage(message);
1835
+ if (response !== null) {
1836
+ writeResponse(output, response);
1837
+ }
1838
+ }
1666
1839
  function writeResponse(output, response) {
1667
1840
  output.write(`${JSON.stringify(response)}
1668
1841
  `);
1669
1842
  }
1670
- var MCP_PROTOCOL_VERSION;
1843
+ var LOCAL_INSTRUCTIONS;
1671
1844
  var init_localMcpStdioServer = __esm({
1672
1845
  "../tools/dist/localMcpStdioServer.js"() {
1673
1846
  "use strict";
1674
1847
  init_mcpAdapter();
1675
- MCP_PROTOCOL_VERSION = "2025-11-25";
1848
+ init_mcpJsonRpcCore();
1849
+ 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
1850
  }
1677
1851
  });
1678
1852
 
@@ -1686,11 +1860,13 @@ __export(dist_exports, {
1686
1860
  createHostedMcpEntrypoint: () => createHostedMcpEntrypoint,
1687
1861
  createLocalMcpStdioServer: () => createLocalMcpStdioServer,
1688
1862
  createMcpAdapter: () => createMcpAdapter,
1863
+ createMcpJsonRpcCore: () => createMcpJsonRpcCore,
1689
1864
  createMcpToolSchemas: () => createMcpToolSchemas,
1690
1865
  createRuntimeToolExecutor: () => createRuntimeToolExecutor,
1691
1866
  isGatedTool: () => isGatedTool,
1692
1867
  isToolAuthorized: () => isToolAuthorized,
1693
- listToolDefinitions: () => listToolDefinitions
1868
+ listToolDefinitions: () => listToolDefinitions,
1869
+ parseErrorResponse: () => parseErrorResponse
1694
1870
  });
1695
1871
  var init_dist = __esm({
1696
1872
  "../tools/dist/index.js"() {
@@ -1700,13 +1876,14 @@ var init_dist = __esm({
1700
1876
  init_hostedMcpEntrypoint();
1701
1877
  init_localMcpStdioServer();
1702
1878
  init_mcpAdapter();
1879
+ init_mcpJsonRpcCore();
1703
1880
  init_gating();
1704
1881
  init_registry();
1705
1882
  }
1706
1883
  });
1707
1884
 
1708
1885
  // src/index.ts
1709
- import { readFile } from "fs/promises";
1886
+ import { readFile as readFile2 } from "fs/promises";
1710
1887
 
1711
1888
  // src/support.ts
1712
1889
  import { createHash } from "crypto";
@@ -2123,6 +2300,9 @@ function withContractVersion(executor, contractVersion) {
2123
2300
  };
2124
2301
  }
2125
2302
 
2303
+ // src/specialCommands.ts
2304
+ import { readFile } from "fs/promises";
2305
+
2126
2306
  // src/doctor.ts
2127
2307
  async function doctor(input) {
2128
2308
  const checks = [
@@ -2287,6 +2467,7 @@ function toolNamesFromResult(result2) {
2287
2467
  // src/simpleCommands.ts
2288
2468
  var knownTopLevelCommands = /* @__PURE__ */ new Set([
2289
2469
  "add",
2470
+ "agent",
2290
2471
  "ask",
2291
2472
  "audit",
2292
2473
  "auth",
@@ -2596,6 +2777,201 @@ function argsWithoutOptions(args) {
2596
2777
  return positionals;
2597
2778
  }
2598
2779
 
2780
+ // src/skill/skillCommands.ts
2781
+ import { access, mkdir as nodeMkdir, writeFile as nodeWriteFile } from "fs/promises";
2782
+ import { homedir } from "os";
2783
+ import { isAbsolute, join, resolve } from "path";
2784
+
2785
+ // src/skill/skillContent.ts
2786
+ 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';
2787
+ 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';
2788
+ 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';
2789
+
2790
+ // src/skill/skillCommands.ts
2791
+ var BUNDLED_SKILLS = [
2792
+ {
2793
+ name: "sift-setup",
2794
+ summary: "Set up the Sift CLI and connect this agent to the brain (first run).",
2795
+ markdown: SIFT_SETUP_SKILL_MARKDOWN
2796
+ },
2797
+ {
2798
+ name: "sift-cli",
2799
+ summary: "Set up and use the Sift CLI as a thin client to the hosted brain.",
2800
+ markdown: SIFT_CLI_SKILL_MARKDOWN
2801
+ },
2802
+ {
2803
+ name: "sift-agent",
2804
+ summary: "Read, capture, and patch the Sift brain over CLI or MCP.",
2805
+ markdown: SIFT_AGENT_SKILL_MARKDOWN
2806
+ }
2807
+ ];
2808
+ var DEFAULT_SKILL = "sift-setup";
2809
+ function defaultSkillIo(input) {
2810
+ return {
2811
+ writeFile: (path, data) => nodeWriteFile(path, data, "utf8"),
2812
+ mkdir: async (path) => {
2813
+ await nodeMkdir(path, { recursive: true });
2814
+ },
2815
+ pathExists: async (path) => {
2816
+ try {
2817
+ await access(path);
2818
+ return true;
2819
+ } catch {
2820
+ return false;
2821
+ }
2822
+ },
2823
+ homeDir: input?.homeDir ?? homedir(),
2824
+ cwd: input?.cwd ?? process.cwd()
2825
+ };
2826
+ }
2827
+ async function runSkillCommand(input) {
2828
+ const [subcommand, ...rest] = input.rest;
2829
+ if (subcommand === void 0 || subcommand === "list") {
2830
+ return listSkills(input.json);
2831
+ }
2832
+ if (subcommand === "print" || subcommand === "show") {
2833
+ return printSkill(rest, input.json);
2834
+ }
2835
+ if (subcommand === "install") {
2836
+ return installSkill(rest, input.json, input.io);
2837
+ }
2838
+ return errorResultWithCode(
2839
+ "tool_unavailable",
2840
+ `Unknown skill subcommand '${subcommand}'. Use 'list', 'print [name]', or 'install [name]'.`,
2841
+ input.json
2842
+ );
2843
+ }
2844
+ function listSkills(json) {
2845
+ if (json) {
2846
+ return ok(
2847
+ `${JSON.stringify({
2848
+ skills: BUNDLED_SKILLS.map(({ name, summary }) => ({ name, summary }))
2849
+ })}
2850
+ `
2851
+ );
2852
+ }
2853
+ const lines = BUNDLED_SKILLS.map((skill) => `${skill.name}: ${skill.summary}`);
2854
+ return ok(`${lines.join("\n")}
2855
+ `);
2856
+ }
2857
+ function printSkill(rest, json) {
2858
+ const requested = positionalArgs(rest)[0];
2859
+ const skill = findSkill(requested);
2860
+ if (skill === void 0) {
2861
+ return unknownSkill(requested, json);
2862
+ }
2863
+ if (json) {
2864
+ return ok(`${JSON.stringify({ skill: skill.name, markdown: skill.markdown })}
2865
+ `);
2866
+ }
2867
+ return ok(withTrailingNewline(skill.markdown));
2868
+ }
2869
+ async function installSkill(rest, json, io) {
2870
+ const requested = positionalArgs(rest)[0];
2871
+ const skill = findSkill(requested);
2872
+ if (skill === void 0) {
2873
+ return unknownSkill(requested, json);
2874
+ }
2875
+ const parsed = parseOptions(rest);
2876
+ const baseDir = resolveBaseDir({ rest, parsed, io });
2877
+ const targetDir = join(baseDir, skill.name);
2878
+ const targetPath = join(targetDir, "SKILL.md");
2879
+ const existed = await io.pathExists(targetPath);
2880
+ await io.mkdir(targetDir);
2881
+ const markdown = withTrailingNewline(skill.markdown);
2882
+ await io.writeFile(targetPath, markdown);
2883
+ const status2 = existed ? "updated" : "created";
2884
+ if (json) {
2885
+ return ok(
2886
+ `${JSON.stringify({
2887
+ installed: true,
2888
+ skill: skill.name,
2889
+ path: targetPath,
2890
+ status: status2,
2891
+ bytes: Buffer.byteLength(markdown, "utf8")
2892
+ })}
2893
+ `
2894
+ );
2895
+ }
2896
+ return ok(renderInstall(skill.name, targetPath, status2));
2897
+ }
2898
+ function resolveBaseDir(input) {
2899
+ const explicit = optionalOption(input.parsed, "dir");
2900
+ if (explicit !== void 0) {
2901
+ return isAbsolute(explicit) ? explicit : resolve(input.io.cwd, explicit);
2902
+ }
2903
+ if (input.rest.includes("--global")) {
2904
+ return join(input.io.homeDir, ".claude", "skills");
2905
+ }
2906
+ return resolve(input.io.cwd, ".claude", "skills");
2907
+ }
2908
+ function findSkill(name) {
2909
+ const target = name ?? DEFAULT_SKILL;
2910
+ return BUNDLED_SKILLS.find((skill) => skill.name === target);
2911
+ }
2912
+ function unknownSkill(name, json) {
2913
+ const available = BUNDLED_SKILLS.map((skill) => skill.name).join(", ");
2914
+ return errorResultWithCode(
2915
+ "validation_failure",
2916
+ `Unknown skill '${name}'. Available skills: ${available}.`,
2917
+ json
2918
+ );
2919
+ }
2920
+ function renderInstall(name, path, status2) {
2921
+ const verb = status2 === "created" ? "Installed" : "Updated";
2922
+ return [
2923
+ `${verb} the Sift skill: ${name}`,
2924
+ `Path: ${path}`,
2925
+ "",
2926
+ "Next, open that SKILL.md and follow it to finish setup:",
2927
+ " 1. Put the CLI on PATH: npm install -g @sift-wiki/cli",
2928
+ " 2. Authenticate: sift login",
2929
+ ' 3. Identify yourself: set SIFT_AGENT to your product name (e.g. "Claude Code"), then sift agent register',
2930
+ " 4. Confirm: sift doctor",
2931
+ "",
2932
+ "Then use Sift as your source of truth: search and assemble context before",
2933
+ "answering, and capture decisions and notes back into the brain.",
2934
+ ""
2935
+ ].join("\n");
2936
+ }
2937
+ function withTrailingNewline(markdown) {
2938
+ return markdown.endsWith("\n") ? markdown : `${markdown}
2939
+ `;
2940
+ }
2941
+
2942
+ // src/specialCommands.ts
2943
+ async function runSpecialCommand(input, args, json, group, command) {
2944
+ if (group === "doctor" && command === void 0) {
2945
+ return doctor({
2946
+ config: input.config,
2947
+ executor: input.executor,
2948
+ json,
2949
+ now: input.now ?? /* @__PURE__ */ new Date()
2950
+ });
2951
+ }
2952
+ if (group === "skill") {
2953
+ const io = input.skillIo ?? defaultSkillIo({ cwd: input.cwd, homeDir: input.homeDir });
2954
+ return runSkillCommand({ rest: args.slice(1), json, io });
2955
+ }
2956
+ const simpleCommand = resolveSimpleCommand({
2957
+ args,
2958
+ json,
2959
+ config: input.config,
2960
+ executor: input.executor,
2961
+ readFile: input.readFile ?? readFile,
2962
+ readStdin: input.readStdin,
2963
+ now: input.now ?? /* @__PURE__ */ new Date()
2964
+ });
2965
+ if (simpleCommand === void 0) return void 0;
2966
+ try {
2967
+ validateAuthenticatedScope(input.config, input.now ?? /* @__PURE__ */ new Date());
2968
+ validateCommandCapability({ commandKey: simpleCommand.commandKey, config: input.config });
2969
+ return await simpleCommand.run();
2970
+ } catch (error) {
2971
+ return errorResult(error, json);
2972
+ }
2973
+ }
2974
+
2599
2975
  // src/toolDiscovery.ts
2600
2976
  function toolsList(input) {
2601
2977
  if (input.executor !== void 0) {
@@ -2730,8 +3106,8 @@ async function runSiftCli(rawInput) {
2730
3106
  json
2731
3107
  }),
2732
3108
  "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),
3109
+ "capture:file": () => captureFile(input.executor, input.readFile ?? readFile2, rest, json),
3110
+ "capture:batch": () => captureBatch(input.executor, input.readFile ?? readFile2, rest, json),
2735
3111
  "source:list": () => sourceList(input.executor, json),
2736
3112
  "source:create": () => sourceCreate(input.executor, rest, json),
2737
3113
  "source:get": () => sourceRead(input.executor, "get", rest, json),
@@ -2742,9 +3118,30 @@ async function runSiftCli(rawInput) {
2742
3118
  "record:create-markdown": () => createMarkdownRecord(input.executor, rest, json),
2743
3119
  "record:patch-section": () => patchRecordSection(input.executor, rest, json),
2744
3120
  "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 }),
3121
+ "evidence:list": () => idTool({
3122
+ executor: input.executor,
3123
+ toolName: "evidence.list",
3124
+ inputKey: "recordId",
3125
+ idLabel: "record ID",
3126
+ rest,
3127
+ json
3128
+ }),
3129
+ "evidence:get": () => idTool({
3130
+ executor: input.executor,
3131
+ toolName: "evidence.get",
3132
+ inputKey: "evidenceId",
3133
+ idLabel: "evidence ID",
3134
+ rest,
3135
+ json
3136
+ }),
3137
+ "graph:neighbors": () => idTool({
3138
+ executor: input.executor,
3139
+ toolName: "graph.neighbors",
3140
+ inputKey: "recordId",
3141
+ idLabel: "record ID",
3142
+ rest,
3143
+ json
3144
+ }),
2748
3145
  "event:list": () => executeSimple2(input.executor, "event.list", {}, json),
2749
3146
  "audit:events": () => auditEvents(input.executor, rest, json),
2750
3147
  "decision:create": () => createDecision(input.executor, rest, json),
@@ -2765,7 +3162,7 @@ async function runSiftCli(rawInput) {
2765
3162
  );
2766
3163
  }
2767
3164
  try {
2768
- if (isAuthCommand(commandKey)) {
3165
+ if (isAuthCommand(commandKey) || commandKey === "mcp:serve") {
2769
3166
  return await handler();
2770
3167
  }
2771
3168
  validateAuthenticatedScope(input.config, input.now ?? /* @__PURE__ */ new Date());
@@ -2775,34 +3172,12 @@ async function runSiftCli(rawInput) {
2775
3172
  return errorResult(error, json);
2776
3173
  }
2777
3174
  }
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.");
3175
+ async function mcpServe(mcpServer, config2, executor, _json) {
3176
+ if (mcpServer === void 0) {
3177
+ return fail("No local MCP server is configured for mcp.serve.");
2803
3178
  }
2804
3179
  if (executor === void 0) {
2805
- return fail("No Sift API executor is configured for mcp.serve.");
3180
+ return fail("Not signed in. Run 'sift login', then 'sift mcp serve'.");
2806
3181
  }
2807
3182
  const result2 = await mcpServer.serve({ config: config2, executor, transport: "local_mcp" });
2808
3183
  if (result2 === void 0) return ok("");
@@ -3127,15 +3502,18 @@ async function idTool(input) {
3127
3502
  }
3128
3503
 
3129
3504
  // src/auth/configStore.ts
3130
- import { mkdir, readFile as readFile2, rm, writeFile, chmod } from "fs/promises";
3131
- import { dirname, join } from "path";
3505
+ import { mkdir, readFile as readFile3, rm, writeFile, chmod } from "fs/promises";
3506
+ import { dirname, join as join2 } from "path";
3507
+ function refreshSlotTokenId(tokenId) {
3508
+ return `refresh:${tokenId}`;
3509
+ }
3132
3510
  function resolveSiftConfigPath(input) {
3133
- return join(input.homeDir, ".sift", "config.json");
3511
+ return join2(input.homeDir, ".sift", "config.json");
3134
3512
  }
3135
3513
  async function readStoredSiftConfig(input) {
3136
3514
  let raw;
3137
3515
  try {
3138
- raw = await readFile2(resolveSiftConfigPath(input), "utf8");
3516
+ raw = await readFile3(resolveSiftConfigPath(input), "utf8");
3139
3517
  } catch (error) {
3140
3518
  if (isNodeError(error) && error.code === "ENOENT") {
3141
3519
  return void 0;
@@ -3168,18 +3546,19 @@ async function loadCliAuthConfig(input) {
3168
3546
  if (profile === void 0) {
3169
3547
  throw new Error(`Stored Sift profile '${stored.currentProfile}' was not found.`);
3170
3548
  }
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
3549
+ const tokenKind = profile.tokenKind ?? "legacy";
3550
+ const expired = Date.parse(profile.tokenExpiresAt) <= input.now.getTime();
3551
+ const token = await resolveStoredToken({
3552
+ homeDir: input.homeDir,
3553
+ credentialStore: input.credentialStore,
3554
+ profile,
3555
+ tokenKind,
3556
+ expired,
3557
+ oauthRefresher: input.oauthRefresher
3177
3558
  });
3178
- if (token === void 0) {
3179
- throw new Error("Stored Sift credential store secret is missing; run `sift login` again.");
3180
- }
3181
3559
  return {
3182
3560
  source: "stored",
3561
+ tokenKind,
3183
3562
  token,
3184
3563
  config: {
3185
3564
  apiBaseUrl: profile.apiBaseUrl,
@@ -3192,6 +3571,63 @@ async function loadCliAuthConfig(input) {
3192
3571
  }
3193
3572
  };
3194
3573
  }
3574
+ async function resolveStoredToken(input) {
3575
+ const { profile } = input;
3576
+ if (input.expired && input.tokenKind === "oauth" && profile.refreshable === true && input.oauthRefresher !== void 0) {
3577
+ return refreshOAuthToken({
3578
+ homeDir: input.homeDir,
3579
+ credentialStore: input.credentialStore,
3580
+ profile,
3581
+ oauthRefresher: input.oauthRefresher
3582
+ });
3583
+ }
3584
+ if (input.expired) {
3585
+ throw new Error("Stored Sift CLI auth has expired; run `sift login` again.");
3586
+ }
3587
+ const token = await input.credentialStore.read({
3588
+ apiBaseUrl: profile.apiBaseUrl,
3589
+ tokenId: profile.tokenId
3590
+ });
3591
+ if (token === void 0) {
3592
+ throw new Error("Stored Sift credential store secret is missing; run `sift login` again.");
3593
+ }
3594
+ return token;
3595
+ }
3596
+ async function refreshOAuthToken(input) {
3597
+ const { profile } = input;
3598
+ const refreshToken = await input.credentialStore.read({
3599
+ apiBaseUrl: profile.apiBaseUrl,
3600
+ tokenId: refreshSlotTokenId(profile.tokenId)
3601
+ });
3602
+ if (refreshToken === void 0) {
3603
+ throw new Error("Stored Sift OAuth refresh token is missing; run `sift login` again.");
3604
+ }
3605
+ const refreshed = await input.oauthRefresher({
3606
+ apiBaseUrl: profile.apiBaseUrl,
3607
+ refreshToken
3608
+ });
3609
+ await input.credentialStore.write({
3610
+ apiBaseUrl: profile.apiBaseUrl,
3611
+ tokenId: profile.tokenId,
3612
+ secret: refreshed.accessToken
3613
+ });
3614
+ if (refreshed.refreshToken !== void 0) {
3615
+ await input.credentialStore.write({
3616
+ apiBaseUrl: profile.apiBaseUrl,
3617
+ tokenId: refreshSlotTokenId(profile.tokenId),
3618
+ secret: refreshed.refreshToken
3619
+ });
3620
+ }
3621
+ const updated = {
3622
+ ...profile,
3623
+ tokenExpiresAt: refreshed.expiresAt ?? profile.tokenExpiresAt
3624
+ };
3625
+ await writeStoredSiftConfig({
3626
+ homeDir: input.homeDir,
3627
+ config: { currentProfile: "default", profiles: { default: updated } }
3628
+ });
3629
+ return refreshed.accessToken;
3630
+ }
3195
3631
  function loadEnvAuth(env, token) {
3196
3632
  return {
3197
3633
  source: "env",
@@ -3222,10 +3658,10 @@ function parseStoredSiftConfig(value) {
3222
3658
  }
3223
3659
  function parseStoredSiftProfile(value) {
3224
3660
  const record = objectValue(value, "profile");
3225
- if ("token" in record || "secret" in record || "tokenSecret" in record) {
3661
+ if ("token" in record || "secret" in record || "tokenSecret" in record || "accessToken" in record || "refreshToken" in record) {
3226
3662
  throw new Error("Stored Sift config must not contain token secrets.");
3227
3663
  }
3228
- return {
3664
+ const profile = {
3229
3665
  apiBaseUrl: stringValue(record.apiBaseUrl, "apiBaseUrl").replace(/\/+$/u, ""),
3230
3666
  appBaseUrl: stringValue(record.appBaseUrl, "appBaseUrl").replace(/\/+$/u, ""),
3231
3667
  workspaceId: stringValue(record.workspaceId, "workspaceId"),
@@ -3236,6 +3672,21 @@ function parseStoredSiftProfile(value) {
3236
3672
  tokenExpiresAt: stringValue(record.tokenExpiresAt, "tokenExpiresAt"),
3237
3673
  capabilities: stringArray(record.capabilities, "capabilities")
3238
3674
  };
3675
+ const tokenKind = tokenKindValue(record.tokenKind);
3676
+ if (tokenKind !== void 0) {
3677
+ profile.tokenKind = tokenKind;
3678
+ }
3679
+ if (record.refreshable === true) {
3680
+ profile.refreshable = true;
3681
+ }
3682
+ return profile;
3683
+ }
3684
+ function tokenKindValue(value) {
3685
+ if (value === void 0) return void 0;
3686
+ if (value === "legacy" || value === "oauth" || value === "service") {
3687
+ return value;
3688
+ }
3689
+ throw new Error("tokenKind must be one of legacy, oauth, service.");
3239
3690
  }
3240
3691
  function requiredEnv(env, name) {
3241
3692
  const value = clean(env[name]);
@@ -3369,21 +3820,115 @@ function isExecError(error) {
3369
3820
 
3370
3821
  // src/auth/loginFlow.ts
3371
3822
  import { execFile as execFile2 } from "child_process";
3372
- import { hostname } from "os";
3823
+ import { hostname as hostname3 } from "os";
3373
3824
  import { promisify as promisify2 } from "util";
3374
3825
 
3826
+ // src/auth/loginHelpers.ts
3827
+ async function resolveLoginApiBaseUrl(input) {
3828
+ const options = parseOptions(input.argv);
3829
+ const fromFlag = clean2(options.get("api-base-url"));
3830
+ if (fromFlag !== void 0) return normalizeUrl(fromFlag);
3831
+ const fromEnv = clean2(input.env.SIFT_API_BASE_URL);
3832
+ if (fromEnv !== void 0) return normalizeUrl(fromEnv);
3833
+ const stored = await readStoredSiftConfig({ homeDir: input.homeDir });
3834
+ const profile = stored?.profiles[stored.currentProfile];
3835
+ if (profile !== void 0) return normalizeUrl(profile.apiBaseUrl);
3836
+ return "https://api.sift.com";
3837
+ }
3838
+ function requestedCapabilities(rest) {
3839
+ const option = parseOptions(rest).get("capability");
3840
+ return option === void 0 ? ["record:read"] : option.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
3841
+ }
3842
+ function errorMessage2(parsed, status2) {
3843
+ if (typeof parsed === "object" && parsed !== null && "error" in parsed) {
3844
+ const error = parsed.error;
3845
+ if (typeof error === "object" && error !== null && "message" in error) {
3846
+ const message = error.message;
3847
+ if (typeof message === "string") return message;
3848
+ }
3849
+ }
3850
+ return `CLI auth request failed with status ${status2}.`;
3851
+ }
3852
+ function normalizeUrl(value) {
3853
+ return value.replace(/\/+$/u, "");
3854
+ }
3855
+ function clean2(value) {
3856
+ const trimmed = value?.trim();
3857
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
3858
+ }
3859
+
3860
+ // src/auth/oauthConfig.ts
3861
+ var TRUE_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
3862
+ function oauthLoginSelected(input) {
3863
+ if (input.argv.includes("--no-oauth")) return false;
3864
+ if (input.argv.includes("--oauth")) return true;
3865
+ const fromEnv = input.env.SIFT_OAUTH?.trim().toLowerCase();
3866
+ return fromEnv !== void 0 && TRUE_VALUES.has(fromEnv);
3867
+ }
3868
+ function resolveCliOAuthConfig(input) {
3869
+ const options = parseOptions(input.argv);
3870
+ const authorizeUrl = clean3(options.get("oauth-authorize-url")) ?? clean3(input.env.SIFT_OAUTH_AUTHORIZE_URL);
3871
+ const tokenUrl = clean3(options.get("oauth-token-url")) ?? clean3(input.env.SIFT_OAUTH_TOKEN_URL);
3872
+ const clientId = clean3(options.get("oauth-client-id")) ?? clean3(input.env.SIFT_OAUTH_CLIENT_ID);
3873
+ if (authorizeUrl === void 0 || tokenUrl === void 0 || clientId === void 0) {
3874
+ return void 0;
3875
+ }
3876
+ const registrationUrl = clean3(options.get("oauth-registration-url")) ?? clean3(input.env.SIFT_OAUTH_REGISTRATION_URL);
3877
+ const config2 = { authorizeUrl, tokenUrl, clientId };
3878
+ if (registrationUrl !== void 0) {
3879
+ config2.registrationUrl = registrationUrl;
3880
+ }
3881
+ const scopes = parseScopeList(clean3(options.get("oauth-scopes")) ?? clean3(input.env.SIFT_OAUTH_SCOPES));
3882
+ if (scopes.length > 0) {
3883
+ config2.defaultScopes = scopes;
3884
+ }
3885
+ return config2;
3886
+ }
3887
+ function scopesForCapabilities(capabilities) {
3888
+ const scopes = /* @__PURE__ */ new Set(["read"]);
3889
+ for (const capability of capabilities) {
3890
+ if (capability.endsWith(":write")) {
3891
+ scopes.add("write");
3892
+ }
3893
+ }
3894
+ return [...scopes];
3895
+ }
3896
+ function parseScopeList(value) {
3897
+ if (value === void 0) return [];
3898
+ return value.split(/[\s,]+/u).map((item) => item.trim()).filter((item) => item.length > 0);
3899
+ }
3900
+ function clean3(value) {
3901
+ const trimmed = value?.trim();
3902
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
3903
+ }
3904
+
3905
+ // src/auth/oauthLoginFlow.ts
3906
+ import { hostname } from "os";
3907
+
3375
3908
  // src/auth/localCallback.ts
3376
3909
  import { createServer } from "http";
3377
3910
  async function createLocalCallbackServer() {
3378
3911
  let resolveCallback;
3379
3912
  let rejectCallback;
3380
- const callbackPromise = new Promise((resolve, reject) => {
3381
- resolveCallback = resolve;
3913
+ const callbackPromise = new Promise((resolve2, reject) => {
3914
+ resolveCallback = resolve2;
3382
3915
  rejectCallback = reject;
3383
3916
  });
3384
3917
  const server = createServer((request, response) => {
3385
3918
  try {
3386
3919
  const url = new URL(request.url ?? "/", "http://127.0.0.1");
3920
+ const error = url.searchParams.get("error");
3921
+ if (error !== null) {
3922
+ const description = url.searchParams.get("error_description");
3923
+ response.writeHead(400, { "Content-Type": "text/plain" });
3924
+ response.end("Sift CLI authorization failed. You can return to the terminal.");
3925
+ rejectCallback?.(
3926
+ new Error(
3927
+ description === null || description.trim().length === 0 ? `Authorization failed: ${error}.` : `Authorization failed: ${error}: ${description}`
3928
+ )
3929
+ );
3930
+ return;
3931
+ }
3387
3932
  const code = url.searchParams.get("code");
3388
3933
  const state = url.searchParams.get("state");
3389
3934
  if (code === null || state === null) {
@@ -3411,21 +3956,21 @@ async function createLocalCallbackServer() {
3411
3956
  };
3412
3957
  }
3413
3958
  function listen(server) {
3414
- return new Promise((resolve, reject) => {
3959
+ return new Promise((resolve2, reject) => {
3415
3960
  server.once("error", reject);
3416
3961
  server.listen(0, "127.0.0.1", () => {
3417
3962
  server.off("error", reject);
3418
- resolve();
3963
+ resolve2();
3419
3964
  });
3420
3965
  });
3421
3966
  }
3422
3967
  function closeServer(server) {
3423
- return new Promise((resolve, reject) => {
3968
+ return new Promise((resolve2, reject) => {
3424
3969
  server.close((error) => {
3425
3970
  if (error) {
3426
3971
  reject(error);
3427
3972
  } else {
3428
- resolve();
3973
+ resolve2();
3429
3974
  }
3430
3975
  });
3431
3976
  });
@@ -3451,15 +3996,445 @@ function sha256Base64Url(value) {
3451
3996
  return createHash2("sha256").update(value).digest("base64url");
3452
3997
  }
3453
3998
 
3999
+ // src/auth/oauthLoginFlow.ts
4000
+ async function oauthBrowserLogin(input) {
4001
+ await input.credentialStore.assertAvailable();
4002
+ const callbackServer = await (input.createCallbackServer ?? createLocalCallbackServer)();
4003
+ try {
4004
+ const pkce = createPkceState({ nextSecret: input.nextSecret });
4005
+ const scopes = mergeScopes(scopesForCapabilities(input.capabilities), input.oauth.defaultScopes);
4006
+ const authorizeUrl = buildAuthorizeUrl({
4007
+ oauth: input.oauth,
4008
+ redirectUri: callbackServer.redirectUri,
4009
+ codeChallenge: pkce.codeChallenge,
4010
+ state: pkce.state,
4011
+ scopes
4012
+ });
4013
+ await tryOpenBrowser(input.openBrowser, authorizeUrl);
4014
+ const callback = await callbackServer.waitForCallback();
4015
+ if (callback.state !== pkce.state) {
4016
+ throw new Error("OAuth callback state mismatch.");
4017
+ }
4018
+ const tokens = await exchangeAuthorizationCode({
4019
+ oauth: input.oauth,
4020
+ fetch: input.fetch,
4021
+ code: callback.code,
4022
+ codeVerifier: pkce.codeVerifier,
4023
+ redirectUri: callbackServer.redirectUri
4024
+ });
4025
+ return finalizeOAuthLogin(input, tokens);
4026
+ } finally {
4027
+ await callbackServer.close();
4028
+ }
4029
+ }
4030
+ async function oauthRefresh(input) {
4031
+ const body = new URLSearchParams({
4032
+ grant_type: "refresh_token",
4033
+ refresh_token: input.refreshToken,
4034
+ client_id: input.oauth.clientId
4035
+ });
4036
+ const tokens = await postForm(input.fetch, input.oauth.tokenUrl, body);
4037
+ return toTokenSet(tokens);
4038
+ }
4039
+ async function finalizeOAuthLogin(input, tokens) {
4040
+ const scope = await input.resolveScope({
4041
+ apiBaseUrl: input.apiBaseUrl,
4042
+ token: tokens.accessToken,
4043
+ fetch: input.fetch
4044
+ });
4045
+ const profile = {
4046
+ apiBaseUrl: input.apiBaseUrl,
4047
+ appBaseUrl: input.appBaseUrl,
4048
+ workspaceId: scope.workspaceId,
4049
+ brainId: scope.brainId,
4050
+ principalId: scope.principalId,
4051
+ // Synthetic, non-secret slot id so the converged token reuses the same
4052
+ // keychain account scheme (apiBaseUrl|tokenId) as the legacy flow.
4053
+ tokenId: "oauth",
4054
+ tokenLabel: tokens.tokenLabel,
4055
+ tokenExpiresAt: tokens.expiresAt ?? farFuture(),
4056
+ capabilities: scope.capabilities,
4057
+ tokenKind: "oauth",
4058
+ refreshable: tokens.refreshToken !== void 0
4059
+ };
4060
+ const result2 = { profile, accessToken: tokens.accessToken };
4061
+ if (tokens.refreshToken !== void 0) {
4062
+ result2.refreshToken = tokens.refreshToken;
4063
+ }
4064
+ return result2;
4065
+ }
4066
+ function buildAuthorizeUrl(input) {
4067
+ const url = new URL(input.oauth.authorizeUrl);
4068
+ url.searchParams.set("response_type", "code");
4069
+ url.searchParams.set("client_id", input.oauth.clientId);
4070
+ url.searchParams.set("redirect_uri", input.redirectUri);
4071
+ url.searchParams.set("code_challenge", input.codeChallenge);
4072
+ url.searchParams.set("code_challenge_method", "S256");
4073
+ url.searchParams.set("state", input.state);
4074
+ if (input.scopes.length > 0) {
4075
+ url.searchParams.set("scope", input.scopes.join(" "));
4076
+ }
4077
+ return url.toString();
4078
+ }
4079
+ async function exchangeAuthorizationCode(input) {
4080
+ const body = new URLSearchParams({
4081
+ grant_type: "authorization_code",
4082
+ code: input.code,
4083
+ redirect_uri: input.redirectUri,
4084
+ client_id: input.oauth.clientId,
4085
+ code_verifier: input.codeVerifier
4086
+ });
4087
+ const tokens = await postForm(input.fetch, input.oauth.tokenUrl, body);
4088
+ return toTokenSet(tokens);
4089
+ }
4090
+ async function postForm(fetchImpl, url, body) {
4091
+ const response = await fetchImpl(url, {
4092
+ method: "POST",
4093
+ headers: {
4094
+ "Content-Type": "application/x-www-form-urlencoded",
4095
+ Accept: "application/json"
4096
+ },
4097
+ body: body.toString()
4098
+ });
4099
+ const text = await response.text();
4100
+ const parsed = text.length === 0 ? {} : JSON.parse(text);
4101
+ if (!response.ok) {
4102
+ throw new Error(oauthTokenError(parsed, response.status));
4103
+ }
4104
+ if (typeof parsed !== "object" || parsed === null) {
4105
+ throw new Error("OAuth token endpoint returned a non-object response.");
4106
+ }
4107
+ return parsed;
4108
+ }
4109
+ function toTokenSet(tokens) {
4110
+ const accessToken = tokens.access_token;
4111
+ if (typeof accessToken !== "string" || accessToken.trim().length === 0) {
4112
+ throw new Error("OAuth token endpoint did not return an access token.");
4113
+ }
4114
+ const set = { accessToken, tokenLabel: oauthTokenLabel() };
4115
+ const refreshToken = tokens.refresh_token;
4116
+ if (typeof refreshToken === "string" && refreshToken.trim().length > 0) {
4117
+ set.refreshToken = refreshToken;
4118
+ }
4119
+ const expiresAt = expiresAtFrom(tokens.expires_in);
4120
+ if (expiresAt !== void 0) {
4121
+ set.expiresAt = expiresAt;
4122
+ }
4123
+ return set;
4124
+ }
4125
+ function expiresAtFrom(expiresIn) {
4126
+ if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
4127
+ return void 0;
4128
+ }
4129
+ return new Date(Date.now() + expiresIn * 1e3).toISOString();
4130
+ }
4131
+ function mergeScopes(derived, defaults) {
4132
+ const merged = new Set(derived);
4133
+ for (const scope of defaults ?? []) {
4134
+ merged.add(scope);
4135
+ }
4136
+ return [...merged];
4137
+ }
4138
+ function oauthTokenLabel() {
4139
+ const name = hostname().trim();
4140
+ return name.length === 0 ? "oauth" : `oauth-${name}`;
4141
+ }
4142
+ function farFuture() {
4143
+ return new Date(Date.now() + 365 * 24 * 60 * 60 * 1e3).toISOString();
4144
+ }
4145
+ async function tryOpenBrowser(openBrowser, url) {
4146
+ if (openBrowser === void 0) return;
4147
+ await openBrowser(url).catch(() => void 0);
4148
+ }
4149
+ function oauthTokenError(parsed, status2) {
4150
+ if (typeof parsed === "object" && parsed !== null) {
4151
+ const record = parsed;
4152
+ const error = typeof record.error === "string" ? record.error : void 0;
4153
+ const description = typeof record.error_description === "string" ? record.error_description : void 0;
4154
+ if (error !== void 0) {
4155
+ return description === void 0 ? `OAuth token request failed: ${error}.` : `OAuth token request failed: ${error}: ${description}`;
4156
+ }
4157
+ }
4158
+ return `OAuth token request failed with status ${status2}.`;
4159
+ }
4160
+
4161
+ // src/auth/serviceTokenLogin.ts
4162
+ import { hostname as hostname2 } from "os";
4163
+ async function serviceTokenLogin(input) {
4164
+ await input.credentialStore.assertAvailable();
4165
+ const callerBearer = await input.resolveCallerBearer();
4166
+ if (callerBearer === void 0) {
4167
+ throw new Error(
4168
+ "Headless login needs an authenticated caller. Set SIFT_API_TOKEN or run 'sift login' once interactively, then retry 'sift login --no-browser'."
4169
+ );
4170
+ }
4171
+ const options = parseOptions(input.rest);
4172
+ const requestBody = buildServiceTokenRequest({
4173
+ rest: input.rest,
4174
+ capabilities: input.capabilities,
4175
+ label: options.get("label"),
4176
+ workspaceId: options.get("workspace-id"),
4177
+ ttlDays: options.get("ttl-days")
4178
+ });
4179
+ const minted = await postServiceTokenMint(
4180
+ input.fetch,
4181
+ `${input.apiBaseUrl}/cli-auth/service-token`,
4182
+ callerBearer,
4183
+ requestBody
4184
+ );
4185
+ const profile = {
4186
+ apiBaseUrl: input.apiBaseUrl,
4187
+ appBaseUrl: input.appBaseUrl,
4188
+ workspaceId: minted.workspaceId,
4189
+ brainId: minted.brainId,
4190
+ principalId: minted.principalId,
4191
+ tokenId: minted.tokenId,
4192
+ tokenLabel: minted.tokenLabel,
4193
+ tokenExpiresAt: minted.tokenExpiresAt,
4194
+ capabilities: minted.capabilities,
4195
+ tokenKind: "service"
4196
+ };
4197
+ return { profile, token: minted.token };
4198
+ }
4199
+ function buildServiceTokenRequest(input) {
4200
+ const body = {
4201
+ label: clean4(input.label) ?? defaultLabel()
4202
+ };
4203
+ const workspaceId = clean4(input.workspaceId);
4204
+ if (workspaceId !== void 0) {
4205
+ body.workspaceId = workspaceId;
4206
+ }
4207
+ if (capabilityFlagPresent(input.rest)) {
4208
+ body.capabilities = input.capabilities;
4209
+ }
4210
+ const ttlDays = clean4(input.ttlDays);
4211
+ if (ttlDays !== void 0) {
4212
+ const parsed = Number(ttlDays);
4213
+ if (!Number.isInteger(parsed) || parsed <= 0) {
4214
+ throw new Error("Option --ttl-days must be a positive integer.");
4215
+ }
4216
+ body.ttlDays = parsed;
4217
+ }
4218
+ return body;
4219
+ }
4220
+ function capabilityFlagPresent(rest) {
4221
+ return rest.includes("--capability");
4222
+ }
4223
+ async function postServiceTokenMint(fetchImpl, url, callerBearer, body) {
4224
+ const response = await fetchImpl(url, {
4225
+ method: "POST",
4226
+ headers: {
4227
+ "Content-Type": "application/json",
4228
+ Authorization: `Bearer ${callerBearer}`
4229
+ },
4230
+ body: JSON.stringify(body)
4231
+ });
4232
+ const text = await response.text();
4233
+ const parsed = text.length === 0 ? {} : JSON.parse(text);
4234
+ if (!response.ok) {
4235
+ throw new Error(serviceTokenError(parsed, response.status));
4236
+ }
4237
+ return assertServiceTokenResponse(parsed);
4238
+ }
4239
+ function assertServiceTokenResponse(parsed) {
4240
+ if (typeof parsed !== "object" || parsed === null) {
4241
+ throw new Error("Service-token mint returned a non-object response.");
4242
+ }
4243
+ const record = parsed;
4244
+ return {
4245
+ token: requiredString(record.token, "token"),
4246
+ tokenId: requiredString(record.tokenId, "tokenId"),
4247
+ tokenLabel: requiredString(record.tokenLabel, "tokenLabel"),
4248
+ tokenExpiresAt: requiredString(record.tokenExpiresAt, "tokenExpiresAt"),
4249
+ workspaceId: requiredString(record.workspaceId, "workspaceId"),
4250
+ brainId: requiredString(record.brainId, "brainId"),
4251
+ principalId: requiredString(record.principalId, "principalId"),
4252
+ capabilities: stringArray2(record.capabilities, "capabilities")
4253
+ };
4254
+ }
4255
+ function requiredString(value, name) {
4256
+ if (typeof value !== "string" || value.trim().length === 0) {
4257
+ throw new Error(`Service-token mint response missing ${name}.`);
4258
+ }
4259
+ return value;
4260
+ }
4261
+ function stringArray2(value, name) {
4262
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
4263
+ throw new Error(`Service-token mint response field ${name} must be a string array.`);
4264
+ }
4265
+ return [...value];
4266
+ }
4267
+ function serviceTokenError(parsed, status2) {
4268
+ if (typeof parsed === "object" && parsed !== null && "error" in parsed) {
4269
+ const error = parsed.error;
4270
+ if (typeof error === "object" && error !== null && "message" in error) {
4271
+ const message = error.message;
4272
+ if (typeof message === "string" && message.trim().length > 0) {
4273
+ return message;
4274
+ }
4275
+ }
4276
+ }
4277
+ return `Service-token mint failed with status ${status2}.`;
4278
+ }
4279
+ function defaultLabel() {
4280
+ const name = hostname2().trim();
4281
+ return name.length === 0 ? "sift-cli-service" : `sift-cli-service-${name}`;
4282
+ }
4283
+ function clean4(value) {
4284
+ const trimmed = value?.trim();
4285
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
4286
+ }
4287
+
4288
+ // src/auth/convergedLogin.ts
4289
+ function oauthRefresherFor(input, rest) {
4290
+ const oauth = input.oauthConfig ?? resolveCliOAuthConfig({ argv: rest, env: input.env });
4291
+ if (oauth === void 0) return void 0;
4292
+ return ({ refreshToken }) => oauthRefresh({ oauth, fetch: input.fetch, refreshToken });
4293
+ }
4294
+ async function oauthBrowserLoginFlow(input, rest, json) {
4295
+ const apiBaseUrl = await resolveLoginApiBaseUrl({ argv: rest, env: input.env, homeDir: input.homeDir });
4296
+ const oauth = resolveOAuthConfigOrThrow(input, rest);
4297
+ const result2 = await oauthBrowserLogin({
4298
+ apiBaseUrl,
4299
+ appBaseUrl: resolveAppBaseUrl(input.env, apiBaseUrl),
4300
+ oauth,
4301
+ capabilities: requestedCapabilities(rest),
4302
+ fetch: input.fetch,
4303
+ credentialStore: input.credentialStore,
4304
+ ...input.openBrowser === void 0 ? {} : { openBrowser: input.openBrowser },
4305
+ ...input.createCallbackServer === void 0 ? {} : { createCallbackServer: input.createCallbackServer },
4306
+ resolveScope: input.resolveScope ?? whoamiResolveScope,
4307
+ ...input.nextSecret === void 0 ? {} : { nextSecret: input.nextSecret }
4308
+ });
4309
+ return persistConvergedLogin(
4310
+ input,
4311
+ {
4312
+ profile: result2.profile,
4313
+ accessToken: result2.accessToken,
4314
+ ...result2.refreshToken === void 0 ? {} : { refreshToken: result2.refreshToken }
4315
+ },
4316
+ json
4317
+ );
4318
+ }
4319
+ async function serviceTokenLoginFlow(input, rest, json) {
4320
+ const apiBaseUrl = await resolveLoginApiBaseUrl({ argv: rest, env: input.env, homeDir: input.homeDir });
4321
+ const result2 = await serviceTokenLogin({
4322
+ apiBaseUrl,
4323
+ appBaseUrl: resolveAppBaseUrl(input.env, apiBaseUrl),
4324
+ rest,
4325
+ capabilities: requestedCapabilities(rest),
4326
+ fetch: input.fetch,
4327
+ credentialStore: input.credentialStore,
4328
+ resolveCallerBearer: defaultCallerBearerResolver(input)
4329
+ });
4330
+ return persistConvergedLogin(input, { profile: result2.profile, accessToken: result2.token }, json);
4331
+ }
4332
+ function resolveOAuthConfigOrThrow(input, rest) {
4333
+ const oauth = input.oauthConfig ?? resolveCliOAuthConfig({ argv: rest, env: input.env });
4334
+ if (oauth === void 0) {
4335
+ throw new Error(
4336
+ "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."
4337
+ );
4338
+ }
4339
+ return oauth;
4340
+ }
4341
+ function defaultCallerBearerResolver(input) {
4342
+ return async () => {
4343
+ const envToken = clean2(input.env.SIFT_API_TOKEN);
4344
+ if (envToken !== void 0) return envToken;
4345
+ const stored = await readStoredSiftConfig({ homeDir: input.homeDir });
4346
+ const profile = stored?.profiles[stored.currentProfile];
4347
+ if (profile === void 0) return void 0;
4348
+ return input.credentialStore.read({
4349
+ apiBaseUrl: profile.apiBaseUrl,
4350
+ tokenId: profile.tokenId
4351
+ });
4352
+ };
4353
+ }
4354
+ var whoamiResolveScope = async ({ apiBaseUrl, token, fetch: fetchImpl }) => {
4355
+ const response = await fetchImpl(`${apiBaseUrl}/agent-tools/whoami`, {
4356
+ method: "POST",
4357
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
4358
+ body: JSON.stringify({ input: {} })
4359
+ });
4360
+ const text = await response.text();
4361
+ const parsed = text.length === 0 ? {} : JSON.parse(text);
4362
+ if (!response.ok) {
4363
+ throw new Error(errorMessage2(parsed, response.status));
4364
+ }
4365
+ return whoamiScopeFrom(parsed);
4366
+ };
4367
+ function whoamiScopeFrom(parsed) {
4368
+ if (typeof parsed !== "object" || parsed === null) {
4369
+ throw new Error("whoami returned a non-object response.");
4370
+ }
4371
+ const record = parsed;
4372
+ const principalId = nestedString(record.principal, "id");
4373
+ const workspaceId = nestedString(record.scope, "workspaceId");
4374
+ const brainId = nestedString(record.scope, "brainId");
4375
+ if (principalId === void 0 || workspaceId === void 0 || brainId === void 0) {
4376
+ throw new Error("whoami response is missing principal or scope fields.");
4377
+ }
4378
+ const capabilities = Array.isArray(record.capabilities) ? record.capabilities.filter((item) => typeof item === "string") : [];
4379
+ return { principalId, workspaceId, brainId, capabilities };
4380
+ }
4381
+ function nestedString(parent, key) {
4382
+ if (typeof parent !== "object" || parent === null) return void 0;
4383
+ const value = parent[key];
4384
+ return typeof value === "string" && value.length > 0 ? value : void 0;
4385
+ }
4386
+ function resolveAppBaseUrl(env, apiBaseUrl) {
4387
+ const fromEnv = clean2(env.SIFT_APP_BASE_URL);
4388
+ if (fromEnv !== void 0) return normalizeUrl(fromEnv);
4389
+ return apiBaseUrl.replace(/\/\/api\./u, "//");
4390
+ }
4391
+ async function persistConvergedLogin(input, result2, json) {
4392
+ const { profile } = result2;
4393
+ try {
4394
+ await input.credentialStore.write({
4395
+ apiBaseUrl: profile.apiBaseUrl,
4396
+ tokenId: profile.tokenId,
4397
+ secret: result2.accessToken
4398
+ });
4399
+ if (result2.refreshToken !== void 0) {
4400
+ await input.credentialStore.write({
4401
+ apiBaseUrl: profile.apiBaseUrl,
4402
+ tokenId: refreshSlotTokenId(profile.tokenId),
4403
+ secret: result2.refreshToken
4404
+ });
4405
+ }
4406
+ } catch (error) {
4407
+ return fail(
4408
+ `Sift CLI login storage failure: ${error instanceof Error ? error.message : "credential store write failed"}`
4409
+ );
4410
+ }
4411
+ await writeStoredSiftConfig({
4412
+ homeDir: input.homeDir,
4413
+ config: { currentProfile: "default", profiles: { default: profile } }
4414
+ });
4415
+ const scope = {
4416
+ apiBaseUrl: profile.apiBaseUrl,
4417
+ tokenLabel: profile.tokenLabel,
4418
+ tokenExpiresAt: profile.tokenExpiresAt,
4419
+ principalId: profile.principalId,
4420
+ workspaceId: profile.workspaceId,
4421
+ brainId: profile.brainId,
4422
+ capabilities: profile.capabilities
4423
+ };
4424
+ return ok(json ? `${JSON.stringify(scope)}
4425
+ ` : `Authenticated Sift CLI
4426
+ ${renderScope(scope)}`);
4427
+ }
4428
+
3454
4429
  // src/auth/loginFlow.ts
3455
4430
  var execFileAsync2 = promisify2(execFile2);
3456
4431
  function createSiftCliAuthCommands(input) {
3457
4432
  const now = input.now ?? (() => /* @__PURE__ */ new Date());
3458
- const sleep = input.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
4433
+ const sleep = input.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
3459
4434
  return {
3460
4435
  async login({ rest, json }) {
3461
4436
  try {
3462
- return rest.includes("--no-browser") ? await deviceLogin(input, rest, sleep, json) : await browserLogin(input, rest, json);
4437
+ return await routeLogin(input, rest, sleep, json);
3463
4438
  } catch (error) {
3464
4439
  return json ? failJson(error instanceof Error ? error.message : "Login failed.") : fail(error instanceof Error ? error.message : "Login failed.");
3465
4440
  }
@@ -3475,21 +4450,18 @@ function createSiftCliAuthCommands(input) {
3475
4450
  env: input.env,
3476
4451
  homeDir: input.homeDir,
3477
4452
  credentialStore: input.credentialStore,
3478
- now: now()
4453
+ now: now(),
4454
+ oauthRefresher: oauthRefresherFor(input, [])
3479
4455
  });
3480
4456
  }
3481
4457
  };
3482
4458
  }
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";
4459
+ async function routeLogin(input, rest, sleep, json) {
4460
+ const noBrowser = rest.includes("--no-browser");
4461
+ if (oauthLoginSelected({ argv: rest, env: input.env })) {
4462
+ return noBrowser ? serviceTokenLoginFlow(input, rest, json) : oauthBrowserLoginFlow(input, rest, json);
4463
+ }
4464
+ return noBrowser ? deviceLogin(input, rest, sleep, json) : browserLogin(input, rest, json);
3493
4465
  }
3494
4466
  async function browserLogin(input, rest, json) {
3495
4467
  await input.credentialStore.assertAvailable();
@@ -3503,10 +4475,10 @@ async function browserLogin(input, rest, json) {
3503
4475
  codeChallenge: pkce.codeChallenge,
3504
4476
  codeChallengeMethod: "S256",
3505
4477
  stateHash: pkce.stateHash,
3506
- deviceLabel: input.deviceLabel ?? hostname(),
4478
+ deviceLabel: input.deviceLabel ?? hostname3(),
3507
4479
  requestedCapabilities: requestedCapabilities(rest)
3508
4480
  });
3509
- await tryOpenBrowser(input.openBrowser, request.authorizeUrl);
4481
+ await tryOpenBrowser2(input.openBrowser, request.authorizeUrl);
3510
4482
  const callback = await callbackServer.waitForCallback();
3511
4483
  if (callback.state !== pkce.stateHash) {
3512
4484
  throw new Error("CLI auth callback state mismatch.");
@@ -3526,7 +4498,7 @@ async function deviceLogin(input, rest, sleep, json) {
3526
4498
  await input.credentialStore.assertAvailable();
3527
4499
  const apiBaseUrl = await resolveLoginApiBaseUrl({ argv: rest, env: input.env, homeDir: input.homeDir });
3528
4500
  const request = await postJson(input.fetch, `${apiBaseUrl}/cli-auth/device`, {
3529
- deviceLabel: input.deviceLabel ?? hostname(),
4501
+ deviceLabel: input.deviceLabel ?? hostname3(),
3530
4502
  requestedCapabilities: requestedCapabilities(rest)
3531
4503
  });
3532
4504
  let intervalSeconds = request.intervalSeconds;
@@ -3652,10 +4624,6 @@ function okStatus(source, loaded, json) {
3652
4624
  ${renderScope(loaded.config)}`
3653
4625
  );
3654
4626
  }
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
4627
  function configFromToken(token) {
3660
4628
  return {
3661
4629
  currentProfile: "default",
@@ -3674,7 +4642,7 @@ function configFromToken(token) {
3674
4642
  }
3675
4643
  };
3676
4644
  }
3677
- async function tryOpenBrowser(openBrowser, url) {
4645
+ async function tryOpenBrowser2(openBrowser, url) {
3678
4646
  await (openBrowser ?? openBrowserUrl)(url).catch(() => void 0);
3679
4647
  }
3680
4648
  async function openBrowserUrl(url) {
@@ -3704,16 +4672,6 @@ async function postJson(fetchImpl, url, body, headers = {}) {
3704
4672
  }
3705
4673
  return parsed;
3706
4674
  }
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
4675
  function failJson(message) {
3718
4676
  return {
3719
4677
  exitCode: 1,
@@ -3722,13 +4680,6 @@ function failJson(message) {
3722
4680
  stderr: ""
3723
4681
  };
3724
4682
  }
3725
- function normalizeUrl(value) {
3726
- return value.replace(/\/+$/u, "");
3727
- }
3728
- function clean2(value) {
3729
- const trimmed = value?.trim();
3730
- return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
3731
- }
3732
4683
 
3733
4684
  // src/bin/sift.ts
3734
4685
  var credentialStore = createMacOSKeychainStore();
@@ -3764,7 +4715,9 @@ var result = await runSiftCli({
3764
4715
  mcpServer: {
3765
4716
  serve: async ({ config: config2, executor }) => {
3766
4717
  if (executor === void 0) {
3767
- throw new Error("No Sift API executor is configured for mcp.serve.");
4718
+ throw new Error(
4719
+ "Not signed in. Run 'sift login' to authenticate, then 'sift mcp serve' to start the local MCP server."
4720
+ );
3768
4721
  }
3769
4722
  const { createLocalMcpStdioServer: createLocalMcpStdioServer2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
3770
4723
  return createLocalMcpStdioServer2({