@sift-wiki/cli 0.1.1 → 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 +25 -4
  2. package/dist/bin/sift.js +1508 -250
  3. package/package.json +11 -9
package/dist/bin/sift.js CHANGED
@@ -9,6 +9,75 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // ../tools/dist/gating.js
13
+ function isGatedTool(name) {
14
+ return !ungatedToolNames.has(name);
15
+ }
16
+ var UNGATED_TOOL_NAMES, ungatedToolNames;
17
+ var init_gating = __esm({
18
+ "../tools/dist/gating.js"() {
19
+ "use strict";
20
+ UNGATED_TOOL_NAMES = [
21
+ "contract.get",
22
+ "whoami",
23
+ "tools.list",
24
+ "tools.schema",
25
+ "tools.help",
26
+ "tools.search",
27
+ "brain.list",
28
+ "brain.use",
29
+ "scope.current"
30
+ ];
31
+ ungatedToolNames = new Set(UNGATED_TOOL_NAMES);
32
+ }
33
+ });
34
+
35
+ // ../tools/dist/canvasTools.js
36
+ function canvasToolDefinitions(writeTool2) {
37
+ return [
38
+ writeTool2("canvas.create", "Create a canvas (a spatial board of live views and notes). Returns the canvas id; compose it with canvas.add_node.", {
39
+ title: { type: "string" },
40
+ intent: { type: "string" }
41
+ }, "sift canvas create --title 'This week'", { required: ["title"] }),
42
+ writeTool2("canvas.add_node", [
43
+ "Add one node to a canvas. Pick the most structured nodeType that fits; prose is the",
44
+ "last resort. nodeType: plan (title + tracks of {name, owners?, goal?, items:[{text,",
45
+ "who?, when?, done?}]} \u2014 roadmaps, weekly plans, agendas), metric (label + value,",
46
+ "optional delta {direction,magnitude,sentiment} \u2014 one number), chart (rows of",
47
+ "{label, value:number}, optional title \u2014 comparing numbers), kanban or checklist",
48
+ "(queryRef binding to a live workspace query), prose (markdown, optional title),",
49
+ "note (text \u2014 a short plain annotation, never primary content).",
50
+ "Content must come from brain data you actually read, never invented."
51
+ ].join(" "), {
52
+ canvasId: { type: "string" },
53
+ nodeType: {
54
+ type: "string",
55
+ enum: ["plan", "metric", "chart", "kanban", "checklist", "prose", "note"]
56
+ },
57
+ text: { type: "string" },
58
+ markdown: { type: "string" },
59
+ title: { type: "string" },
60
+ timeframe: { type: "string" },
61
+ tracks: { type: "array", items: { type: "object" } },
62
+ label: { type: "string" },
63
+ value: { type: "string" },
64
+ delta: { type: "object" },
65
+ rows: { type: "array", items: { type: "object" } },
66
+ queryRef: { type: "string" },
67
+ position: { type: "object" }
68
+ }, "sift canvas add-node <canvas-id> --type metric", { required: ["canvasId", "nodeType"] }),
69
+ writeTool2("canvas.remove_node", "Remove a node or note from a canvas by id.", {
70
+ canvasId: { type: "string" },
71
+ nodeId: { type: "string" }
72
+ }, "sift canvas remove-node <canvas-id> <node-id>", { required: ["canvasId", "nodeId"] })
73
+ ];
74
+ }
75
+ var init_canvasTools = __esm({
76
+ "../tools/dist/canvasTools.js"() {
77
+ "use strict";
78
+ }
79
+ });
80
+
12
81
  // ../tools/dist/inputParsers.js
13
82
  function parseCaptureFile(input) {
14
83
  return {
@@ -68,6 +137,8 @@ function parseSearchQuery(input) {
68
137
  function parseContextQuery(input) {
69
138
  return {
70
139
  query: requireString(input, "query"),
140
+ queryIssuedAt: optionalString(input, "queryIssuedAt"),
141
+ timezone: optionalString(input, "timezone"),
71
142
  maxChars: requireInteger(input, "maxChars", 4e3)
72
143
  };
73
144
  }
@@ -247,7 +318,38 @@ var init_inputParsers = __esm({
247
318
 
248
319
  // ../tools/dist/registry.js
249
320
  function listToolDefinitions() {
250
- return [...toolDefinitions];
321
+ return [...toolDefinitions, ...canvasToolDefinitions(writeTool)].map((tool) => isGatedTool(tool.name) ? withContractVersionProperty(tool) : tool);
322
+ }
323
+ function withContractVersionProperty(tool) {
324
+ return {
325
+ ...tool,
326
+ inputSchema: {
327
+ ...tool.inputSchema,
328
+ properties: {
329
+ ...tool.inputSchema.properties,
330
+ contractVersion: {
331
+ type: "string",
332
+ description: "Echo the current Sift contract version from contract.get."
333
+ }
334
+ }
335
+ }
336
+ };
337
+ }
338
+ function isToolAuthorized(capabilities, tool) {
339
+ return tool.capability === NO_CAPABILITY || capabilities.includes(tool.capability);
340
+ }
341
+ function identityTool(name, summary, properties, cliExample, options) {
342
+ return defineTool({
343
+ name,
344
+ summary,
345
+ properties,
346
+ required: options.required ?? [],
347
+ capability: NO_CAPABILITY,
348
+ mutability: options.mutability,
349
+ transports: writeTransports,
350
+ cliExample,
351
+ hostedAgent: { available: false, ...options.hostedAgent }
352
+ });
251
353
  }
252
354
  function readTool(name, summary, properties, cliExample, options) {
253
355
  return defineTool({
@@ -275,6 +377,19 @@ function writeTool(name, summary, properties, cliExample, options) {
275
377
  hostedAgent: options?.hostedAgent
276
378
  });
277
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
+ }
278
393
  function sourceWriteTool(name, summary, properties, cliExample) {
279
394
  return defineTool({
280
395
  name,
@@ -338,31 +453,8 @@ function defaultRiskClass(mutability) {
338
453
  return "low";
339
454
  }
340
455
  function defaultToolsets(name) {
341
- const [prefix] = name.split(".");
342
- switch (prefix) {
343
- case "decision":
344
- case "task":
345
- return ["work"];
346
- case "skill":
347
- return ["brain", "work"];
348
- case "record":
349
- case "source":
350
- case "capture":
351
- case "ingestion":
352
- return ["brain", "ingestion"];
353
- case "search":
354
- case "context":
355
- case "evidence":
356
- case "graph":
357
- return ["brain", "retrieval"];
358
- case "tools":
359
- return ["registry"];
360
- case "audit":
361
- case "event":
362
- return ["audit"];
363
- default:
364
- return ["brain"];
365
- }
456
+ const [prefix = ""] = name.split(".");
457
+ return defaultToolsetsByPrefix[prefix] ?? ["brain"];
366
458
  }
367
459
  function defaultSearchTerms(name, summary) {
368
460
  return [.../* @__PURE__ */ new Set([...tokenize(name), ...tokenize(summary)])];
@@ -373,14 +465,35 @@ function tokenize(text) {
373
465
  function stringProps(names) {
374
466
  return Object.fromEntries(names.map((name) => [name, { type: "string" }]));
375
467
  }
376
- var readTransports, writeTransports, toolDefinitions;
468
+ var readTransports, writeTransports, NO_CAPABILITY, defaultToolsetsByPrefix, toolDefinitions;
377
469
  var init_registry = __esm({
378
470
  "../tools/dist/registry.js"() {
379
471
  "use strict";
472
+ init_gating();
473
+ init_canvasTools();
380
474
  init_inputParsers();
381
475
  readTransports = ["cli", "hosted_mcp", "local_mcp"];
382
476
  writeTransports = ["cli", "hosted_mcp", "local_mcp"];
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
+ };
383
495
  toolDefinitions = [
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"),
384
497
  readTool("whoami", "Return principal, actor, scope, and capabilities.", {}, "sift whoami"),
385
498
  readTool("brain.list", "List brains available to the current scope.", {}, "sift brain list"),
386
499
  readTool("brain.use", "Select the current brain scope for subsequent operations.", stringProps(["brainId"]), "sift brain use <brain-id>"),
@@ -465,8 +578,15 @@ var init_registry = __esm({
465
578
  evidenceIds: { type: "array", items: { type: "string" } },
466
579
  visibility: { type: "array", items: { type: "string" } }
467
580
  }, "sift task create", { required: ["title", "visibility"] }),
581
+ identityTool("agent.register", "Register the calling agent as a workspace agent worker acting for the token's owner; idempotent on (workspace, owner, normalized name) and never mutates token state.", {
582
+ name: { type: "string", maxLength: 80 },
583
+ description: { type: "string", maxLength: 280 },
584
+ kind: { type: "string" }
585
+ }, 'sift agent register --name "Claude Code" --description "Coding agent"', { required: ["name"], mutability: "write" }),
586
+ identityTool("agent.status", "Report the request's resolved agent identity (from the asserted agent name) or none.", {}, "sift agent status --json", { mutability: "read" }),
468
587
  readTool("skill.resolve", "Resolve at most three advisory skill candidates for a task description, each with the skill record id, pinned active version id, title, and applicability summary, or an empty list.", { query: { type: "string" } }, "sift skill resolve 'draft the monthly investor update'", { required: ["query"] }),
469
- readTool("skill.get", "Read a skill's pinned active version markdown body and version id by skill record id.", { skillId: { type: "string" } }, "sift skill get <skill-id>", { required: ["skillId"] }),
588
+ readTool("skill.get", "Read a skill's pinned active version markdown body, version id, and bundle file paths by skill record id.", { skillId: { type: "string" } }, "sift skill get <skill-id>", { required: ["skillId"] }),
589
+ readTool("skill.file", "Read one of a skill's bundle files by path; fetch a file only when the skill body references it and the task needs that detail, using a path listed by skill.get.", { skillId: { type: "string" }, path: { type: "string" } }, "sift skill file <skill-id> examples/2026-05-29-good.md", { required: ["skillId", "path"] }),
470
590
  writeTool("skill.exercise", "Report that a skill version informed an output on a surface; the report is an attribution claim only.", {
471
591
  skillId: { type: "string" },
472
592
  versionId: { type: "string" },
@@ -490,16 +610,68 @@ var init_registry = __esm({
490
610
  severity: { type: "string" },
491
611
  visibility: { type: "array", items: { type: "string" } }
492
612
  }, "sift skill teach <skill-id> --lesson 'when X, do Y'", { required: ["skillId", "lesson", "visibility"] }),
493
- 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.", {
494
614
  query: { type: "string" },
495
615
  limit: { type: "integer", minimum: 1, maximum: 20 }
496
616
  }, "sift search query 'launch risks'"),
497
- 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"],
498
624
  hostedAgent: {
499
625
  toolsets: ["brain", "retrieval"],
500
626
  searchTerms: ["context", "cite", "answer", "evidence"]
501
627
  }
502
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
+ }),
503
675
  readTool("context.profile", "Read a permission-filtered profile context model.", {}, "sift context profile"),
504
676
  readTool("evidence.list", "List authorized evidence links for a record.", stringProps(["recordId"]), "sift evidence list <record-id>"),
505
677
  readTool("evidence.get", "Read an authorized evidence item.", stringProps(["evidenceId"]), "sift evidence get <evidence-id>"),
@@ -515,6 +687,7 @@ function executeWhoami(auth) {
515
687
  return {
516
688
  principal: { id: auth.principalId, type: auth.principalType },
517
689
  actor: { id: auth.actorId, type: auth.actorType },
690
+ ...auth.agent === void 0 ? {} : { agent: auth.agent },
518
691
  scope: { workspaceId: auth.workspaceId, brainId: auth.brainId },
519
692
  requestId: auth.requestId,
520
693
  authPath: auth.authPath,
@@ -581,7 +754,7 @@ function executeToolsSearch(input, searchInput) {
581
754
  function availableTools(input) {
582
755
  const transport = input.transport ?? "local_mcp";
583
756
  const availableNames = new Set(input.availableToolNames ?? IMPLEMENTED_TOOL_NAMES);
584
- return listToolDefinitions().filter((tool) => availableNames.has(tool.name) && tool.transports.includes(transport) && input.auth.capabilities.includes(tool.capability));
757
+ return listToolDefinitions().filter((tool) => availableNames.has(tool.name) && tool.transports.includes(transport) && isToolAuthorized(input.auth.capabilities, tool));
585
758
  }
586
759
  function scoreTool(tool, intentTokens) {
587
760
  const haystack = /* @__PURE__ */ new Set([
@@ -601,6 +774,7 @@ var init_discovery = __esm({
601
774
  "use strict";
602
775
  init_registry();
603
776
  IMPLEMENTED_TOOL_NAMES = [
777
+ "contract.get",
604
778
  "whoami",
605
779
  "brain.list",
606
780
  "brain.use",
@@ -631,7 +805,9 @@ var init_discovery = __esm({
631
805
  "evidence.get",
632
806
  "graph.neighbors",
633
807
  "event.list",
634
- "audit.events"
808
+ "audit.events",
809
+ "agent.register",
810
+ "agent.status"
635
811
  ];
636
812
  }
637
813
  });
@@ -704,6 +880,26 @@ var init_captureTools = __esm({
704
880
  }
705
881
  });
706
882
 
883
+ // ../tools/dist/contractTools.js
884
+ function contractToolHandlers(input) {
885
+ return {
886
+ "contract.get": () => {
887
+ if (input.service.getContract === void 0) {
888
+ throw new Error("Tool 'contract.get' requires a runtime service contract.");
889
+ }
890
+ return input.service.getContract({ auth: input.auth });
891
+ }
892
+ };
893
+ }
894
+ function contractToolAvailability(service) {
895
+ return [[service.getContract !== void 0, ["contract.get"]]];
896
+ }
897
+ var init_contractTools = __esm({
898
+ "../tools/dist/contractTools.js"() {
899
+ "use strict";
900
+ }
901
+ });
902
+
707
903
  // ../tools/dist/skillTools.js
708
904
  function skillToolHandlers(input, toolInput) {
709
905
  return {
@@ -723,6 +919,15 @@ function skillToolHandlers(input, toolInput) {
723
919
  skillId: requireString(toolInput, "skillId")
724
920
  });
725
921
  },
922
+ "skill.file": () => {
923
+ if (input.service.getSkillFile === void 0)
924
+ throw missingSkillService("skill.file");
925
+ return input.service.getSkillFile({
926
+ auth: input.auth,
927
+ skillId: requireString(toolInput, "skillId"),
928
+ path: requireString(toolInput, "path")
929
+ });
930
+ },
726
931
  "skill.exercise": () => {
727
932
  if (input.service.recordSkillExercise === void 0) {
728
933
  throw missingSkillService("skill.exercise");
@@ -743,6 +948,7 @@ function skillToolAvailability(service) {
743
948
  return [
744
949
  [service.resolveSkills !== void 0, ["skill.resolve"]],
745
950
  [service.getSkill !== void 0, ["skill.get"]],
951
+ [service.getSkillFile !== void 0, ["skill.file"]],
746
952
  [service.recordSkillExercise !== void 0, ["skill.exercise"]],
747
953
  [service.teachSkill !== void 0, ["skill.teach"]]
748
954
  ];
@@ -805,6 +1011,106 @@ var init_skillTools = __esm({
805
1011
  }
806
1012
  });
807
1013
 
1014
+ // ../tools/dist/agentIdentityTools.js
1015
+ function agentIdentityToolHandlers(input, toolInput) {
1016
+ return {
1017
+ "agent.register": () => {
1018
+ if (input.service.registerAgent === void 0) {
1019
+ throw missingAgentIdentityService("agent.register");
1020
+ }
1021
+ return input.service.registerAgent({
1022
+ auth: input.auth,
1023
+ name: requireString(toolInput, "name"),
1024
+ description: optionalString(toolInput, "description"),
1025
+ kind: parseAgentKind(toolInput)
1026
+ });
1027
+ },
1028
+ "agent.status": () => {
1029
+ if (input.service.getAgentStatus === void 0) {
1030
+ throw missingAgentIdentityService("agent.status");
1031
+ }
1032
+ return input.service.getAgentStatus({ auth: input.auth });
1033
+ }
1034
+ };
1035
+ }
1036
+ function agentIdentityToolAvailability(service) {
1037
+ return [
1038
+ [service.registerAgent !== void 0, ["agent.register"]],
1039
+ [service.getAgentStatus !== void 0, ["agent.status"]]
1040
+ ];
1041
+ }
1042
+ function parseAgentKind(toolInput) {
1043
+ const kind = optionalString(toolInput, "kind");
1044
+ if (kind === void 0) {
1045
+ return void 0;
1046
+ }
1047
+ if (kind !== "agent" && kind !== "service") {
1048
+ throw new Error("kind must be 'agent' or 'service'.");
1049
+ }
1050
+ return kind;
1051
+ }
1052
+ function missingAgentIdentityService(toolName) {
1053
+ return new Error(`Tool '${toolName}' requires a runtime service contract.`);
1054
+ }
1055
+ var init_agentIdentityTools = __esm({
1056
+ "../tools/dist/agentIdentityTools.js"() {
1057
+ "use strict";
1058
+ init_inputParsers();
1059
+ }
1060
+ });
1061
+
1062
+ // ../tools/dist/toolAvailability.js
1063
+ function runtimeAvailableToolNames(service) {
1064
+ const baseNames = [
1065
+ "whoami",
1066
+ "tools.list",
1067
+ "tools.schema",
1068
+ "tools.help",
1069
+ "tools.search",
1070
+ "brain.list",
1071
+ "brain.use",
1072
+ "scope.current",
1073
+ "capture.text",
1074
+ "capture.batch",
1075
+ "search.query",
1076
+ "context.assemble"
1077
+ ];
1078
+ const optionalNames = [
1079
+ [service.ingestFile !== void 0, ["capture.file"]],
1080
+ [service.createDecision !== void 0, ["decision.create"]],
1081
+ [service.createTask !== void 0, ["task.create"]],
1082
+ ...skillToolAvailability(service),
1083
+ ...agentIdentityToolAvailability(service),
1084
+ ...contractToolAvailability(service),
1085
+ [service.listSources !== void 0, ["source.list"]],
1086
+ [service.createSource !== void 0, ["source.create"]],
1087
+ [service.getSource !== void 0, ["source.get", "source.status"]],
1088
+ [service.getIngestionStatus !== void 0, ["ingestion.status"]],
1089
+ [service.listRecords !== void 0, ["record.list"]],
1090
+ [service.getRecord !== void 0, ["record.get"]],
1091
+ [service.createMarkdownRecord !== void 0, ["record.create_markdown"]],
1092
+ [service.patchRecordSection !== void 0, ["record.patch_section"]],
1093
+ [service.listRecordVersions !== void 0, ["record.versions"]],
1094
+ [service.listEvidence !== void 0, ["evidence.list"]],
1095
+ [service.getEvidence !== void 0, ["evidence.get"]],
1096
+ [service.listGraphNeighbors !== void 0, ["graph.neighbors"]],
1097
+ [service.listEvents !== void 0, ["event.list"]],
1098
+ [service.getContextProfile !== void 0, ["context.profile"]],
1099
+ [service.listAuditEvents !== void 0, ["audit.events"]],
1100
+ [service.webSearch !== void 0, ["web.search"]],
1101
+ [service.webFetch !== void 0, ["web.fetch"]]
1102
+ ];
1103
+ return [...baseNames, ...optionalNames.flatMap(([enabled, names]) => enabled ? names : [])];
1104
+ }
1105
+ var init_toolAvailability = __esm({
1106
+ "../tools/dist/toolAvailability.js"() {
1107
+ "use strict";
1108
+ init_agentIdentityTools();
1109
+ init_contractTools();
1110
+ init_skillTools();
1111
+ }
1112
+ });
1113
+
808
1114
  // ../tools/dist/toolLog.js
809
1115
  function logToolCall(input) {
810
1116
  input.onToolLog?.({
@@ -870,6 +1176,73 @@ var init_toolLog = __esm({
870
1176
  }
871
1177
  });
872
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
+
873
1246
  // ../tools/dist/executor.js
874
1247
  function createRuntimeToolExecutor(input) {
875
1248
  const availableToolNames = runtimeAvailableToolNames(input.service);
@@ -937,7 +1310,10 @@ function createToolHandlers(input, toolInput) {
937
1310
  "context.profile": () => executeContextProfile(input, toolInput),
938
1311
  "decision.create": () => executeDecisionCreate(input, toolInput),
939
1312
  "task.create": () => executeTaskCreate(input, toolInput),
1313
+ ...webToolHandlers(input, toolInput),
940
1314
  ...skillToolHandlers(input, toolInput),
1315
+ ...agentIdentityToolHandlers(input, toolInput),
1316
+ ...contractToolHandlers(input),
941
1317
  "source.list": () => executeSourceList(input),
942
1318
  "source.create": () => executeSourceCreate(input, toolInput),
943
1319
  "source.get": () => executeSourceRead(input, toolInput, "source.get"),
@@ -980,44 +1356,6 @@ function createToolHandlers(input, toolInput) {
980
1356
  "audit.events": () => executeAuditEvents(input, toolInput)
981
1357
  };
982
1358
  }
983
- function runtimeAvailableToolNames(service) {
984
- const baseNames = [
985
- "whoami",
986
- "tools.list",
987
- "tools.schema",
988
- "tools.help",
989
- "tools.search",
990
- "brain.list",
991
- "brain.use",
992
- "scope.current",
993
- "capture.text",
994
- "capture.batch",
995
- "search.query",
996
- "context.assemble"
997
- ];
998
- const optionalNames = [
999
- [service.ingestFile !== void 0, ["capture.file"]],
1000
- [service.createDecision !== void 0, ["decision.create"]],
1001
- [service.createTask !== void 0, ["task.create"]],
1002
- ...skillToolAvailability(service),
1003
- [service.listSources !== void 0, ["source.list"]],
1004
- [service.createSource !== void 0, ["source.create"]],
1005
- [service.getSource !== void 0, ["source.get", "source.status"]],
1006
- [service.getIngestionStatus !== void 0, ["ingestion.status"]],
1007
- [service.listRecords !== void 0, ["record.list"]],
1008
- [service.getRecord !== void 0, ["record.get"]],
1009
- [service.createMarkdownRecord !== void 0, ["record.create_markdown"]],
1010
- [service.patchRecordSection !== void 0, ["record.patch_section"]],
1011
- [service.listRecordVersions !== void 0, ["record.versions"]],
1012
- [service.listEvidence !== void 0, ["evidence.list"]],
1013
- [service.getEvidence !== void 0, ["evidence.get"]],
1014
- [service.listGraphNeighbors !== void 0, ["graph.neighbors"]],
1015
- [service.listEvents !== void 0, ["event.list"]],
1016
- [service.getContextProfile !== void 0, ["context.profile"]],
1017
- [service.listAuditEvents !== void 0, ["audit.events"]]
1018
- ];
1019
- return [...baseNames, ...optionalNames.flatMap(([enabled, names]) => enabled ? names : [])];
1020
- }
1021
1359
  function missingService(toolName) {
1022
1360
  return new Error(`Tool '${toolName}' requires a runtime service contract.`);
1023
1361
  }
@@ -1032,6 +1370,21 @@ function executeSearchQuery(input, toolInput) {
1032
1370
  }
1033
1371
  function executeContextAssemble(input, toolInput) {
1034
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
+ }
1035
1388
  return input.service.retrieveCitedContext({
1036
1389
  auth: input.auth,
1037
1390
  query: query.query,
@@ -1039,6 +1392,13 @@ function executeContextAssemble(input, toolInput) {
1039
1392
  maxChars: query.maxChars
1040
1393
  });
1041
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
+ }
1042
1402
  function executeContextProfile(input, toolInput) {
1043
1403
  if (input.service.getContextProfile === void 0) {
1044
1404
  throw new Error("Tool 'context.profile' requires a profile read service contract.");
@@ -1144,9 +1504,13 @@ var init_executor = __esm({
1144
1504
  init_discovery();
1145
1505
  init_captureTools();
1146
1506
  init_inputParsers();
1507
+ init_contractTools();
1147
1508
  init_skillTools();
1509
+ init_agentIdentityTools();
1510
+ init_toolAvailability();
1148
1511
  init_results();
1149
1512
  init_toolLog();
1513
+ init_webToolRuntime();
1150
1514
  }
1151
1515
  });
1152
1516
 
@@ -1169,7 +1533,7 @@ function createCliCommandMetadata(filter) {
1169
1533
  }
1170
1534
  function availableTools2(filter) {
1171
1535
  const availableNames = new Set(filter.toolNames ?? IMPLEMENTED_TOOL_NAMES);
1172
- return listToolDefinitions().filter((tool) => availableNames.has(tool.name) && tool.transports.includes(filter.transport) && filter.capabilities.includes(tool.capability));
1536
+ return listToolDefinitions().filter((tool) => availableNames.has(tool.name) && tool.transports.includes(filter.transport) && isToolAuthorized(filter.capabilities, tool));
1173
1537
  }
1174
1538
  var init_generated = __esm({
1175
1539
  "../tools/dist/generated.js"() {
@@ -1185,7 +1549,7 @@ function createMcpAdapter(input) {
1185
1549
  ...IMPLEMENTED_TOOL_NAMES
1186
1550
  ];
1187
1551
  const availableNameSet = new Set(availableToolNames);
1188
- 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));
1189
1553
  return {
1190
1554
  listTools() {
1191
1555
  return createMcpToolSchemas({
@@ -1222,6 +1586,9 @@ function renderToolResult(result2) {
1222
1586
  }
1223
1587
  function classifyToolError(error) {
1224
1588
  if (error instanceof Error) {
1589
+ if (error.message.startsWith("Sift contract required.")) {
1590
+ return "contract_required";
1591
+ }
1225
1592
  if (isPermissionError2(error)) {
1226
1593
  return "permission_denied";
1227
1594
  }
@@ -1334,89 +1701,41 @@ var init_hostedMcpEntrypoint = __esm({
1334
1701
  }
1335
1702
  });
1336
1703
 
1337
- // ../tools/dist/localMcpStdioServer.js
1338
- function createLocalMcpStdioServer(input) {
1704
+ // ../tools/dist/mcpJsonRpcCore.js
1705
+ function createMcpJsonRpcCore(input) {
1706
+ const { adapter, config: config2 } = input;
1339
1707
  return {
1340
- async serve(serverInput) {
1341
- const adapter = createMcpAdapter({
1342
- transport: "local_mcp",
1343
- capabilities: serverInput.capabilities,
1344
- executor: serverInput.executor
1345
- });
1346
- let buffer = "";
1347
- input.input.setEncoding("utf8");
1348
- for await (const chunk of input.input) {
1349
- buffer += chunk;
1350
- let newline = buffer.indexOf("\n");
1351
- while (newline >= 0) {
1352
- const line = buffer.slice(0, newline).trim();
1353
- buffer = buffer.slice(newline + 1);
1354
- if (line.length > 0) {
1355
- await handleLine(line, adapter, input.output, input.error);
1356
- }
1357
- newline = buffer.indexOf("\n");
1358
- }
1708
+ async handleMessage(message) {
1709
+ if (message.id === void 0) {
1710
+ return null;
1359
1711
  }
1360
- const trailing = buffer.trim();
1361
- if (trailing.length > 0) {
1362
- 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");
1363
1724
  }
1364
1725
  }
1365
1726
  };
1366
1727
  }
1367
- async function handleLine(line, adapter, output, error) {
1368
- let message;
1369
- try {
1370
- message = JSON.parse(line);
1371
- } catch {
1372
- writeResponse(output, {
1373
- jsonrpc: "2.0",
1374
- id: null,
1375
- error: { code: -32700, message: "Parse error" }
1376
- });
1377
- return;
1378
- }
1379
- if (message.id === void 0) {
1380
- if (message.method === "notifications/initialized")
1381
- return;
1382
- error?.write(`Ignoring MCP notification '${String(message.method)}'.
1383
- `);
1384
- return;
1385
- }
1386
- const id = normalizeId(message.id);
1387
- if (message.jsonrpc !== "2.0" || typeof message.method !== "string") {
1388
- writeResponse(output, {
1389
- jsonrpc: "2.0",
1390
- id,
1391
- error: { code: -32600, message: "Invalid Request" }
1392
- });
1393
- return;
1394
- }
1395
- try {
1396
- writeResponse(output, {
1397
- jsonrpc: "2.0",
1398
- id,
1399
- result: await dispatchRequest(message.method, message.params, adapter)
1400
- });
1401
- } catch (err) {
1402
- writeResponse(output, {
1403
- jsonrpc: "2.0",
1404
- id,
1405
- error: {
1406
- code: -32601,
1407
- message: err instanceof Error ? err.message : "Method not found"
1408
- }
1409
- });
1410
- }
1728
+ function parseErrorResponse() {
1729
+ return errorResponse(null, -32700, "Parse error");
1411
1730
  }
1412
- async function dispatchRequest(method, params, adapter) {
1731
+ async function dispatchRequest(method, params, adapter, config2) {
1413
1732
  if (method === "initialize") {
1414
1733
  const requested = readProtocolVersion(params);
1415
1734
  return {
1416
1735
  protocolVersion: requested ?? MCP_PROTOCOL_VERSION,
1417
1736
  capabilities: { tools: { listChanged: false } },
1418
- serverInfo: { name: "sift-local-mcp", version: "0.1.0" },
1419
- instructions: "Use Sift tools to read and write the hosted canonical brain."
1737
+ serverInfo: { name: config2.serverName, version: config2.version },
1738
+ instructions: config2.instructions
1420
1739
  };
1421
1740
  }
1422
1741
  if (method === "ping")
@@ -1427,7 +1746,7 @@ async function dispatchRequest(method, params, adapter) {
1427
1746
  const call = parseToolCall(params);
1428
1747
  return adapter.callTool(call);
1429
1748
  }
1430
- throw new Error(`Method '${method}' is not supported by Sift local MCP.`);
1749
+ throw new Error(`Method '${method}' is not supported by Sift MCP.`);
1431
1750
  }
1432
1751
  function readProtocolVersion(params) {
1433
1752
  if (!isRecord(params))
@@ -1443,6 +1762,9 @@ function parseToolCall(params) {
1443
1762
  arguments: isRecord(params.arguments) ? params.arguments : {}
1444
1763
  };
1445
1764
  }
1765
+ function errorResponse(id, code, message) {
1766
+ return { jsonrpc: "2.0", id, error: { code, message } };
1767
+ }
1446
1768
  function isRecord(value) {
1447
1769
  return typeof value === "object" && value !== null && !Array.isArray(value);
1448
1770
  }
@@ -1451,16 +1773,80 @@ function normalizeId(value) {
1451
1773
  return value;
1452
1774
  return null;
1453
1775
  }
1454
- function writeResponse(output, response) {
1455
- output.write(`${JSON.stringify(response)}
1456
- `);
1457
- }
1458
1776
  var MCP_PROTOCOL_VERSION;
1459
- var init_localMcpStdioServer = __esm({
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
+ }
1839
+ function writeResponse(output, response) {
1840
+ output.write(`${JSON.stringify(response)}
1841
+ `);
1842
+ }
1843
+ var LOCAL_INSTRUCTIONS;
1844
+ var init_localMcpStdioServer = __esm({
1460
1845
  "../tools/dist/localMcpStdioServer.js"() {
1461
1846
  "use strict";
1462
1847
  init_mcpAdapter();
1463
- 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.";
1464
1850
  }
1465
1851
  });
1466
1852
 
@@ -1468,13 +1854,19 @@ var init_localMcpStdioServer = __esm({
1468
1854
  var dist_exports = {};
1469
1855
  __export(dist_exports, {
1470
1856
  MCP_PROTOCOL_VERSION: () => MCP_PROTOCOL_VERSION,
1857
+ NO_CAPABILITY: () => NO_CAPABILITY,
1858
+ UNGATED_TOOL_NAMES: () => UNGATED_TOOL_NAMES,
1471
1859
  createCliCommandMetadata: () => createCliCommandMetadata,
1472
1860
  createHostedMcpEntrypoint: () => createHostedMcpEntrypoint,
1473
1861
  createLocalMcpStdioServer: () => createLocalMcpStdioServer,
1474
1862
  createMcpAdapter: () => createMcpAdapter,
1863
+ createMcpJsonRpcCore: () => createMcpJsonRpcCore,
1475
1864
  createMcpToolSchemas: () => createMcpToolSchemas,
1476
1865
  createRuntimeToolExecutor: () => createRuntimeToolExecutor,
1477
- listToolDefinitions: () => listToolDefinitions
1866
+ isGatedTool: () => isGatedTool,
1867
+ isToolAuthorized: () => isToolAuthorized,
1868
+ listToolDefinitions: () => listToolDefinitions,
1869
+ parseErrorResponse: () => parseErrorResponse
1478
1870
  });
1479
1871
  var init_dist = __esm({
1480
1872
  "../tools/dist/index.js"() {
@@ -1484,12 +1876,14 @@ var init_dist = __esm({
1484
1876
  init_hostedMcpEntrypoint();
1485
1877
  init_localMcpStdioServer();
1486
1878
  init_mcpAdapter();
1879
+ init_mcpJsonRpcCore();
1880
+ init_gating();
1487
1881
  init_registry();
1488
1882
  }
1489
1883
  });
1490
1884
 
1491
1885
  // src/index.ts
1492
- import { readFile } from "fs/promises";
1886
+ import { readFile as readFile2 } from "fs/promises";
1493
1887
 
1494
1888
  // src/support.ts
1495
1889
  import { createHash } from "crypto";
@@ -1720,6 +2114,9 @@ function fail(message) {
1720
2114
  }
1721
2115
  function classifyError(error) {
1722
2116
  if (error instanceof Error) {
2117
+ if (error.message.startsWith("Sift contract required.")) {
2118
+ return "contract_required";
2119
+ }
1723
2120
  if (isPermissionError(error)) {
1724
2121
  return "permission_denied";
1725
2122
  }
@@ -1768,6 +2165,41 @@ function addReceiptLine(lines, label, value) {
1768
2165
  }
1769
2166
  }
1770
2167
 
2168
+ // src/agentCommands.ts
2169
+ async function agentRegister(executor, assertedAgentName, rest, json) {
2170
+ if (executor === void 0) {
2171
+ return fail("No Sift API executor is configured for agent.register.");
2172
+ }
2173
+ const parsed = parseOptions(rest);
2174
+ const name = optionalOption(parsed, "name") ?? assertedAgentName;
2175
+ if (name === void 0 || name.trim().length === 0) {
2176
+ return fail("Missing agent name: pass --name or set SIFT_AGENT.");
2177
+ }
2178
+ const input = { name };
2179
+ const description = optionalOption(parsed, "description");
2180
+ if (description !== void 0) {
2181
+ input.description = description;
2182
+ }
2183
+ const kind = optionalOption(parsed, "kind");
2184
+ if (kind !== void 0) {
2185
+ input.kind = kind;
2186
+ }
2187
+ const result2 = await executor.execute("agent.register", input);
2188
+ return ok(json ? `${JSON.stringify(result2)}
2189
+ ` : renderAgentRegisterResult(result2));
2190
+ }
2191
+ function renderAgentRegisterResult(result2) {
2192
+ if (typeof result2 !== "object" || result2 === null || !("agent" in result2)) {
2193
+ return `${JSON.stringify(result2)}
2194
+ `;
2195
+ }
2196
+ const { agent, created, reactivated } = result2;
2197
+ const verb = created === true ? "Registered" : reactivated === true ? "Reactivated" : "Already registered";
2198
+ const actsFor = agent.actsForDisplayName === void 0 ? "" : ` (acting for ${agent.actsForDisplayName})`;
2199
+ return `${verb} agent worker '${agent.name ?? "unknown"}'${actsFor}.
2200
+ `;
2201
+ }
2202
+
1771
2203
  // src/auth/commandAdapter.ts
1772
2204
  function isAuthCommand(commandKey) {
1773
2205
  return commandKey === "login:" || commandKey === "auth:status" || commandKey === "logout:";
@@ -1791,6 +2223,7 @@ function authCommand(authCommands2, command, input) {
1791
2223
 
1792
2224
  // src/capabilityGuard.ts
1793
2225
  var commandCapabilities = {
2226
+ "contract:get": "record:read",
1794
2227
  "whoami:": "record:read",
1795
2228
  "brain:list": "record:read",
1796
2229
  "brain:use": "record:read",
@@ -1838,6 +2271,38 @@ function validateCommandCapability(input) {
1838
2271
  }
1839
2272
  }
1840
2273
 
2274
+ // src/contractOption.ts
2275
+ function extractContractVersion(argv2) {
2276
+ const index = argv2.indexOf("--contract");
2277
+ if (index === -1) {
2278
+ return { argv: argv2 };
2279
+ }
2280
+ const value = argv2[index + 1];
2281
+ if (value === void 0 || value.trim().length === 0 || value.startsWith("--")) {
2282
+ return { argv: [...argv2.slice(0, index), ...argv2.slice(index + 1)] };
2283
+ }
2284
+ return {
2285
+ argv: [...argv2.slice(0, index), ...argv2.slice(index + 2)],
2286
+ contractVersion: value
2287
+ };
2288
+ }
2289
+ function applyContractOption(input) {
2290
+ const { argv: argv2, contractVersion } = extractContractVersion(input.argv);
2291
+ if (contractVersion === void 0 || input.executor === void 0) {
2292
+ return { ...input, argv: argv2 };
2293
+ }
2294
+ return { ...input, argv: argv2, executor: withContractVersion(input.executor, contractVersion) };
2295
+ }
2296
+ function withContractVersion(executor, contractVersion) {
2297
+ return {
2298
+ ...executor,
2299
+ execute: (name, toolInput) => executor.execute(name, { ...toolInput, contractVersion })
2300
+ };
2301
+ }
2302
+
2303
+ // src/specialCommands.ts
2304
+ import { readFile } from "fs/promises";
2305
+
1841
2306
  // src/doctor.ts
1842
2307
  async function doctor(input) {
1843
2308
  const checks = [
@@ -2002,12 +2467,14 @@ function toolNamesFromResult(result2) {
2002
2467
  // src/simpleCommands.ts
2003
2468
  var knownTopLevelCommands = /* @__PURE__ */ new Set([
2004
2469
  "add",
2470
+ "agent",
2005
2471
  "ask",
2006
2472
  "audit",
2007
2473
  "auth",
2008
2474
  "brain",
2009
2475
  "capture",
2010
2476
  "context",
2477
+ "contract",
2011
2478
  "decision",
2012
2479
  "decide",
2013
2480
  "doctor",
@@ -2310,6 +2777,201 @@ function argsWithoutOptions(args) {
2310
2777
  return positionals;
2311
2778
  }
2312
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
+
2313
2975
  // src/toolDiscovery.ts
2314
2976
  function toolsList(input) {
2315
2977
  if (input.executor !== void 0) {
@@ -2360,7 +3022,8 @@ function createHostedApiExecutor(input) {
2360
3022
  authorization: `Bearer ${input.token}`,
2361
3023
  "content-type": "application/json",
2362
3024
  "x-sift-brain-id": input.brainId,
2363
- "x-sift-workspace-id": input.workspaceId
3025
+ "x-sift-workspace-id": input.workspaceId,
3026
+ ...input.agentName === void 0 || input.agentName.trim().length === 0 ? {} : { "x-sift-agent-name": input.agentName.trim() }
2364
3027
  },
2365
3028
  body: JSON.stringify({ input: toolInput }, serializeJsonValue)
2366
3029
  });
@@ -2403,7 +3066,8 @@ function errorMessage(parsed, status2) {
2403
3066
  }
2404
3067
 
2405
3068
  // src/index.ts
2406
- async function runSiftCli(input) {
3069
+ async function runSiftCli(rawInput) {
3070
+ const input = applyContractOption(rawInput);
2407
3071
  const json = input.argv.includes("--json");
2408
3072
  const args = input.argv.filter((arg) => arg !== "--json");
2409
3073
  const [group, command, ...rest] = args;
@@ -2412,6 +3076,7 @@ async function runSiftCli(input) {
2412
3076
  const commandKey = group === "login" ? "login:" : `${group ?? ""}:${command ?? ""}`;
2413
3077
  const commandRest = group === "login" ? args.slice(1) : rest;
2414
3078
  const handlers = {
3079
+ "contract:get": () => executeSimple2(rawInput.executor, "contract.get", {}, json),
2415
3080
  "whoami:": () => executeSimple2(input.executor, "whoami", {}, json),
2416
3081
  "brain:list": () => executeSimple2(input.executor, "brain.list", {}, json),
2417
3082
  "brain:use": () => idTool({
@@ -2441,8 +3106,8 @@ async function runSiftCli(input) {
2441
3106
  json
2442
3107
  }),
2443
3108
  "capture:text": () => captureText(input.executor, rest, json),
2444
- "capture:file": () => captureFile(input.executor, input.readFile ?? readFile, rest, json),
2445
- "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),
2446
3111
  "source:list": () => sourceList(input.executor, json),
2447
3112
  "source:create": () => sourceCreate(input.executor, rest, json),
2448
3113
  "source:get": () => sourceRead(input.executor, "get", rest, json),
@@ -2453,14 +3118,37 @@ async function runSiftCli(input) {
2453
3118
  "record:create-markdown": () => createMarkdownRecord(input.executor, rest, json),
2454
3119
  "record:patch-section": () => patchRecordSection(input.executor, rest, json),
2455
3120
  "record:versions": () => recordRead(input.executor, "record.versions", rest, json),
2456
- "evidence:list": () => idTool({ executor: input.executor, toolName: "evidence.list", inputKey: "recordId", idLabel: "record ID", rest, json }),
2457
- "evidence:get": () => idTool({ executor: input.executor, toolName: "evidence.get", inputKey: "evidenceId", idLabel: "evidence ID", rest, json }),
2458
- "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
+ }),
2459
3145
  "event:list": () => executeSimple2(input.executor, "event.list", {}, json),
2460
3146
  "audit:events": () => auditEvents(input.executor, rest, json),
2461
3147
  "decision:create": () => createDecision(input.executor, rest, json),
2462
3148
  "task:create": () => createTask(input.executor, rest, json),
2463
- "mcp:serve": () => mcpServe(input.mcpServer, input.config, input.executor, json),
3149
+ "agent:register": () => agentRegister(input.executor, input.agentName, rest, json),
3150
+ "agent:status": () => executeSimple2(input.executor, "agent.status", {}, json),
3151
+ "mcp:serve": () => mcpServe(input.mcpServer, input.config, rawInput.executor, json),
2464
3152
  "login:": () => authCommand(input.authCommands, "login", { rest: commandRest, json }),
2465
3153
  "auth:status": () => authCommand(input.authCommands, "status", { json }),
2466
3154
  "logout:": () => authCommand(input.authCommands, "logout", { json })
@@ -2474,7 +3162,7 @@ async function runSiftCli(input) {
2474
3162
  );
2475
3163
  }
2476
3164
  try {
2477
- if (isAuthCommand(commandKey)) {
3165
+ if (isAuthCommand(commandKey) || commandKey === "mcp:serve") {
2478
3166
  return await handler();
2479
3167
  }
2480
3168
  validateAuthenticatedScope(input.config, input.now ?? /* @__PURE__ */ new Date());
@@ -2484,39 +3172,16 @@ async function runSiftCli(input) {
2484
3172
  return errorResult(error, json);
2485
3173
  }
2486
3174
  }
2487
- async function runSpecialCommand(input, args, json, group, command) {
2488
- if (group === "doctor" && command === void 0) {
2489
- return doctor({ config: input.config, executor: input.executor, json, now: input.now ?? /* @__PURE__ */ new Date() });
2490
- }
2491
- const simpleCommand = resolveSimpleCommand({
2492
- args,
2493
- json,
2494
- config: input.config,
2495
- executor: input.executor,
2496
- readFile: input.readFile ?? readFile,
2497
- readStdin: input.readStdin,
2498
- now: input.now ?? /* @__PURE__ */ new Date()
2499
- });
2500
- if (simpleCommand === void 0) return void 0;
2501
- try {
2502
- validateAuthenticatedScope(input.config, input.now ?? /* @__PURE__ */ new Date());
2503
- validateCommandCapability({ commandKey: simpleCommand.commandKey, config: input.config });
2504
- return await simpleCommand.run();
2505
- } catch (error) {
2506
- return errorResult(error, json);
2507
- }
2508
- }
2509
- async function mcpServe(mcpServer, config2, executor, json) {
3175
+ async function mcpServe(mcpServer, config2, executor, _json) {
2510
3176
  if (mcpServer === void 0) {
2511
3177
  return fail("No local MCP server is configured for mcp.serve.");
2512
3178
  }
2513
3179
  if (executor === void 0) {
2514
- return fail("No Sift API executor is configured for mcp.serve.");
3180
+ return fail("Not signed in. Run 'sift login', then 'sift mcp serve'.");
2515
3181
  }
2516
3182
  const result2 = await mcpServer.serve({ config: config2, executor, transport: "local_mcp" });
2517
3183
  if (result2 === void 0) return ok("");
2518
- return ok(json ? `${JSON.stringify(result2)}
2519
- ` : `${JSON.stringify(result2)}
3184
+ return ok(`${JSON.stringify(result2)}
2520
3185
  `);
2521
3186
  }
2522
3187
  function scopeCurrent(config2, json) {
@@ -2837,15 +3502,18 @@ async function idTool(input) {
2837
3502
  }
2838
3503
 
2839
3504
  // src/auth/configStore.ts
2840
- import { mkdir, readFile as readFile2, rm, writeFile, chmod } from "fs/promises";
2841
- 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
+ }
2842
3510
  function resolveSiftConfigPath(input) {
2843
- return join(input.homeDir, ".sift", "config.json");
3511
+ return join2(input.homeDir, ".sift", "config.json");
2844
3512
  }
2845
3513
  async function readStoredSiftConfig(input) {
2846
3514
  let raw;
2847
3515
  try {
2848
- raw = await readFile2(resolveSiftConfigPath(input), "utf8");
3516
+ raw = await readFile3(resolveSiftConfigPath(input), "utf8");
2849
3517
  } catch (error) {
2850
3518
  if (isNodeError(error) && error.code === "ENOENT") {
2851
3519
  return void 0;
@@ -2878,18 +3546,19 @@ async function loadCliAuthConfig(input) {
2878
3546
  if (profile === void 0) {
2879
3547
  throw new Error(`Stored Sift profile '${stored.currentProfile}' was not found.`);
2880
3548
  }
2881
- if (Date.parse(profile.tokenExpiresAt) <= input.now.getTime()) {
2882
- throw new Error("Stored Sift CLI auth has expired; run `sift login` again.");
2883
- }
2884
- const token = await input.credentialStore.read({
2885
- apiBaseUrl: profile.apiBaseUrl,
2886
- 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
2887
3558
  });
2888
- if (token === void 0) {
2889
- throw new Error("Stored Sift credential store secret is missing; run `sift login` again.");
2890
- }
2891
3559
  return {
2892
3560
  source: "stored",
3561
+ tokenKind,
2893
3562
  token,
2894
3563
  config: {
2895
3564
  apiBaseUrl: profile.apiBaseUrl,
@@ -2902,6 +3571,63 @@ async function loadCliAuthConfig(input) {
2902
3571
  }
2903
3572
  };
2904
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
+ }
2905
3631
  function loadEnvAuth(env, token) {
2906
3632
  return {
2907
3633
  source: "env",
@@ -2932,10 +3658,10 @@ function parseStoredSiftConfig(value) {
2932
3658
  }
2933
3659
  function parseStoredSiftProfile(value) {
2934
3660
  const record = objectValue(value, "profile");
2935
- 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) {
2936
3662
  throw new Error("Stored Sift config must not contain token secrets.");
2937
3663
  }
2938
- return {
3664
+ const profile = {
2939
3665
  apiBaseUrl: stringValue(record.apiBaseUrl, "apiBaseUrl").replace(/\/+$/u, ""),
2940
3666
  appBaseUrl: stringValue(record.appBaseUrl, "appBaseUrl").replace(/\/+$/u, ""),
2941
3667
  workspaceId: stringValue(record.workspaceId, "workspaceId"),
@@ -2946,6 +3672,21 @@ function parseStoredSiftProfile(value) {
2946
3672
  tokenExpiresAt: stringValue(record.tokenExpiresAt, "tokenExpiresAt"),
2947
3673
  capabilities: stringArray(record.capabilities, "capabilities")
2948
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.");
2949
3690
  }
2950
3691
  function requiredEnv(env, name) {
2951
3692
  const value = clean(env[name]);
@@ -3079,21 +3820,115 @@ function isExecError(error) {
3079
3820
 
3080
3821
  // src/auth/loginFlow.ts
3081
3822
  import { execFile as execFile2 } from "child_process";
3082
- import { hostname } from "os";
3823
+ import { hostname as hostname3 } from "os";
3083
3824
  import { promisify as promisify2 } from "util";
3084
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
+
3085
3908
  // src/auth/localCallback.ts
3086
3909
  import { createServer } from "http";
3087
3910
  async function createLocalCallbackServer() {
3088
3911
  let resolveCallback;
3089
3912
  let rejectCallback;
3090
- const callbackPromise = new Promise((resolve, reject) => {
3091
- resolveCallback = resolve;
3913
+ const callbackPromise = new Promise((resolve2, reject) => {
3914
+ resolveCallback = resolve2;
3092
3915
  rejectCallback = reject;
3093
3916
  });
3094
3917
  const server = createServer((request, response) => {
3095
3918
  try {
3096
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
+ }
3097
3932
  const code = url.searchParams.get("code");
3098
3933
  const state = url.searchParams.get("state");
3099
3934
  if (code === null || state === null) {
@@ -3121,21 +3956,21 @@ async function createLocalCallbackServer() {
3121
3956
  };
3122
3957
  }
3123
3958
  function listen(server) {
3124
- return new Promise((resolve, reject) => {
3959
+ return new Promise((resolve2, reject) => {
3125
3960
  server.once("error", reject);
3126
3961
  server.listen(0, "127.0.0.1", () => {
3127
3962
  server.off("error", reject);
3128
- resolve();
3963
+ resolve2();
3129
3964
  });
3130
3965
  });
3131
3966
  }
3132
3967
  function closeServer(server) {
3133
- return new Promise((resolve, reject) => {
3968
+ return new Promise((resolve2, reject) => {
3134
3969
  server.close((error) => {
3135
3970
  if (error) {
3136
3971
  reject(error);
3137
3972
  } else {
3138
- resolve();
3973
+ resolve2();
3139
3974
  }
3140
3975
  });
3141
3976
  });
@@ -3161,15 +3996,445 @@ function sha256Base64Url(value) {
3161
3996
  return createHash2("sha256").update(value).digest("base64url");
3162
3997
  }
3163
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
+
3164
4429
  // src/auth/loginFlow.ts
3165
4430
  var execFileAsync2 = promisify2(execFile2);
3166
4431
  function createSiftCliAuthCommands(input) {
3167
4432
  const now = input.now ?? (() => /* @__PURE__ */ new Date());
3168
- const sleep = input.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
4433
+ const sleep = input.sleep ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
3169
4434
  return {
3170
4435
  async login({ rest, json }) {
3171
4436
  try {
3172
- return rest.includes("--no-browser") ? await deviceLogin(input, rest, sleep, json) : await browserLogin(input, rest, json);
4437
+ return await routeLogin(input, rest, sleep, json);
3173
4438
  } catch (error) {
3174
4439
  return json ? failJson(error instanceof Error ? error.message : "Login failed.") : fail(error instanceof Error ? error.message : "Login failed.");
3175
4440
  }
@@ -3185,21 +4450,18 @@ function createSiftCliAuthCommands(input) {
3185
4450
  env: input.env,
3186
4451
  homeDir: input.homeDir,
3187
4452
  credentialStore: input.credentialStore,
3188
- now: now()
4453
+ now: now(),
4454
+ oauthRefresher: oauthRefresherFor(input, [])
3189
4455
  });
3190
4456
  }
3191
4457
  };
3192
4458
  }
3193
- async function resolveLoginApiBaseUrl(input) {
3194
- const options = parseOptions(input.argv);
3195
- const fromFlag = clean2(options.get("api-base-url"));
3196
- if (fromFlag !== void 0) return normalizeUrl(fromFlag);
3197
- const fromEnv = clean2(input.env.SIFT_API_BASE_URL);
3198
- if (fromEnv !== void 0) return normalizeUrl(fromEnv);
3199
- const stored = await readStoredSiftConfig({ homeDir: input.homeDir });
3200
- const profile = stored?.profiles[stored.currentProfile];
3201
- if (profile !== void 0) return normalizeUrl(profile.apiBaseUrl);
3202
- 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);
3203
4465
  }
3204
4466
  async function browserLogin(input, rest, json) {
3205
4467
  await input.credentialStore.assertAvailable();
@@ -3213,10 +4475,10 @@ async function browserLogin(input, rest, json) {
3213
4475
  codeChallenge: pkce.codeChallenge,
3214
4476
  codeChallengeMethod: "S256",
3215
4477
  stateHash: pkce.stateHash,
3216
- deviceLabel: input.deviceLabel ?? hostname(),
4478
+ deviceLabel: input.deviceLabel ?? hostname3(),
3217
4479
  requestedCapabilities: requestedCapabilities(rest)
3218
4480
  });
3219
- await tryOpenBrowser(input.openBrowser, request.authorizeUrl);
4481
+ await tryOpenBrowser2(input.openBrowser, request.authorizeUrl);
3220
4482
  const callback = await callbackServer.waitForCallback();
3221
4483
  if (callback.state !== pkce.stateHash) {
3222
4484
  throw new Error("CLI auth callback state mismatch.");
@@ -3236,7 +4498,7 @@ async function deviceLogin(input, rest, sleep, json) {
3236
4498
  await input.credentialStore.assertAvailable();
3237
4499
  const apiBaseUrl = await resolveLoginApiBaseUrl({ argv: rest, env: input.env, homeDir: input.homeDir });
3238
4500
  const request = await postJson(input.fetch, `${apiBaseUrl}/cli-auth/device`, {
3239
- deviceLabel: input.deviceLabel ?? hostname(),
4501
+ deviceLabel: input.deviceLabel ?? hostname3(),
3240
4502
  requestedCapabilities: requestedCapabilities(rest)
3241
4503
  });
3242
4504
  let intervalSeconds = request.intervalSeconds;
@@ -3362,10 +4624,6 @@ function okStatus(source, loaded, json) {
3362
4624
  ${renderScope(loaded.config)}`
3363
4625
  );
3364
4626
  }
3365
- function requestedCapabilities(rest) {
3366
- const option = parseOptions(rest).get("capability");
3367
- return option === void 0 ? ["record:read"] : option.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
3368
- }
3369
4627
  function configFromToken(token) {
3370
4628
  return {
3371
4629
  currentProfile: "default",
@@ -3384,7 +4642,7 @@ function configFromToken(token) {
3384
4642
  }
3385
4643
  };
3386
4644
  }
3387
- async function tryOpenBrowser(openBrowser, url) {
4645
+ async function tryOpenBrowser2(openBrowser, url) {
3388
4646
  await (openBrowser ?? openBrowserUrl)(url).catch(() => void 0);
3389
4647
  }
3390
4648
  async function openBrowserUrl(url) {
@@ -3414,16 +4672,6 @@ async function postJson(fetchImpl, url, body, headers = {}) {
3414
4672
  }
3415
4673
  return parsed;
3416
4674
  }
3417
- function errorMessage2(parsed, status2) {
3418
- if (typeof parsed === "object" && parsed !== null && "error" in parsed) {
3419
- const error = parsed.error;
3420
- if (typeof error === "object" && error !== null && "message" in error) {
3421
- const message = error.message;
3422
- if (typeof message === "string") return message;
3423
- }
3424
- }
3425
- return `CLI auth request failed with status ${status2}.`;
3426
- }
3427
4675
  function failJson(message) {
3428
4676
  return {
3429
4677
  exitCode: 1,
@@ -3432,13 +4680,6 @@ function failJson(message) {
3432
4680
  stderr: ""
3433
4681
  };
3434
4682
  }
3435
- function normalizeUrl(value) {
3436
- return value.replace(/\/+$/u, "");
3437
- }
3438
- function clean2(value) {
3439
- const trimmed = value?.trim();
3440
- return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
3441
- }
3442
4683
 
3443
4684
  // src/bin/sift.ts
3444
4685
  var credentialStore = createMacOSKeychainStore();
@@ -3457,21 +4698,26 @@ var config = loadedAuth?.config ?? {
3457
4698
  principalId: "",
3458
4699
  capabilities: []
3459
4700
  };
4701
+ var { argv, agentName } = extractAgentName(process.argv.slice(2), process.env.SIFT_AGENT);
3460
4702
  var result = await runSiftCli({
3461
- argv: process.argv.slice(2),
4703
+ argv,
3462
4704
  config,
3463
4705
  readStdin,
4706
+ agentName,
3464
4707
  executor: loadedAuth === void 0 ? void 0 : createHostedApiExecutor({
3465
4708
  apiBaseUrl: loadedAuth.config.apiBaseUrl,
3466
4709
  token: loadedAuth.token,
3467
4710
  workspaceId: loadedAuth.config.workspaceId,
3468
- brainId: loadedAuth.config.brainId
4711
+ brainId: loadedAuth.config.brainId,
4712
+ agentName
3469
4713
  }),
3470
4714
  authCommands,
3471
4715
  mcpServer: {
3472
4716
  serve: async ({ config: config2, executor }) => {
3473
4717
  if (executor === void 0) {
3474
- 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
+ );
3475
4721
  }
3476
4722
  const { createLocalMcpStdioServer: createLocalMcpStdioServer2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
3477
4723
  return createLocalMcpStdioServer2({
@@ -3488,6 +4734,18 @@ var result = await runSiftCli({
3488
4734
  process.stdout.write(result.stdout);
3489
4735
  process.stderr.write(result.stderr);
3490
4736
  process.exitCode = result.exitCode;
4737
+ function extractAgentName(args, envAgentName) {
4738
+ const flagIndex = args.indexOf("--as-agent");
4739
+ if (flagIndex !== -1 && args[flagIndex + 1] !== void 0) {
4740
+ const name = args[flagIndex + 1];
4741
+ return {
4742
+ argv: [...args.slice(0, flagIndex), ...args.slice(flagIndex + 2)],
4743
+ agentName: name
4744
+ };
4745
+ }
4746
+ const env = envAgentName?.trim();
4747
+ return { argv: args, agentName: env === void 0 || env.length === 0 ? void 0 : env };
4748
+ }
3491
4749
  async function readStdin() {
3492
4750
  const chunks = [];
3493
4751
  for await (const chunk of process.stdin) {