@lore-co/cli 0.1.18 → 0.1.19

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.
package/dist/cli.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { access, chmod, copyFile, mkdir, open, readFile, readdir, rename, rm, stat, writeFile, } from "node:fs/promises";
3
3
  import { constants as fsConstants, realpathSync } from "node:fs";
4
+ import { spawn } from "node:child_process";
4
5
  import { createHash, randomUUID } from "node:crypto";
5
6
  import { delimiter, dirname, resolve } from "node:path";
6
- import { homedir, platform } from "node:os";
7
+ import { homedir, hostname, platform } from "node:os";
8
+ import { setTimeout as delay } from "node:timers/promises";
7
9
  import { fileURLToPath, pathToFileURL } from "node:url";
8
- import { CLAUDE_RELIABILITY_INTEGRATION, CODEX_RELIABILITY_INTEGRATION, CURSOR_RELIABILITY_INTEGRATION, NATIVE_CODING_AGENT_NAMES, OPENCODE_RELIABILITY_INTEGRATION, OperationalMetricsResponseSchema, POLYTOKEN_RELIABILITY_INTEGRATION, RELIABILITY_INTEGRATION_CONTRACT_VERSION, ReliabilityInvocationRecordSchema, WorkspaceIdentityResponseSchema, canonicalJson, deriveReliabilityInvocationHealth, isNativeCodingAgent, } from "@lore-co/core";
10
+ import { CLAUDE_RELIABILITY_INTEGRATION, CODEX_RELIABILITY_INTEGRATION, COPILOT_CLI_RELIABILITY_INTEGRATION, COPILOT_CLOUD_MCP_RELIABILITY_INTEGRATION, COPILOT_VSCODE_PREVIEW_RELIABILITY_INTEGRATION, CreateDeviceAuthorizationResponseSchema, CURSOR_RELIABILITY_INTEGRATION, NATIVE_CODING_AGENT_NAMES, OPENCODE_RELIABILITY_INTEGRATION, OperationalMetricsResponseSchema, PollDeviceAuthorizationResponseSchema, POLYTOKEN_RELIABILITY_INTEGRATION, RELIABILITY_INTEGRATION_CONTRACT_VERSION, ReliabilityInvocationRecordSchema, WorkspaceIdentityResponseSchema, canonicalJson, deriveReliabilityInvocationHealth, isNativeCodingAgent, } from "@lore-co/core";
9
11
  import { runHook, } from "./runtime.js";
10
12
  import { readInvocationHealthWriteInput, runInvocationHealthWrite, } from "./invocation-health-writer.js";
11
13
  import { runAskCommand } from "./ask.js";
@@ -44,9 +46,26 @@ const POLYTOKEN_HOOK_EVENTS = [
44
46
  "pre_user_prompt",
45
47
  "post_model_turn",
46
48
  ];
49
+ const COPILOT_CLI_HOOK_EVENTS = [
50
+ "userPromptTransformed",
51
+ "preToolUse",
52
+ "agentStop",
53
+ "sessionEnd",
54
+ ];
55
+ const EXTERNAL_SURFACES = [
56
+ {
57
+ surface: "copilot-cloud-mcp",
58
+ integrationId: COPILOT_CLOUD_MCP_RELIABILITY_INTEGRATION.integrationId,
59
+ installation: "repository_template",
60
+ locallyInspectable: false,
61
+ capabilities: COPILOT_CLOUD_MCP_RELIABILITY_INTEGRATION.capabilities,
62
+ },
63
+ ];
47
64
  const AGENT_MANIFESTS = {
48
65
  claude: CLAUDE_RELIABILITY_INTEGRATION,
49
66
  codex: CODEX_RELIABILITY_INTEGRATION,
67
+ "copilot-cli": COPILOT_CLI_RELIABILITY_INTEGRATION,
68
+ "copilot-vscode": COPILOT_VSCODE_PREVIEW_RELIABILITY_INTEGRATION,
50
69
  cursor: CURSOR_RELIABILITY_INTEGRATION,
51
70
  opencode: OPENCODE_RELIABILITY_INTEGRATION,
52
71
  polytoken: POLYTOKEN_RELIABILITY_INTEGRATION,
@@ -102,21 +121,26 @@ Examples:
102
121
  lore devin --help
103
122
  `;
104
123
  const CONNECT_HELP = `lore connect
105
- Store a workspace credential and idempotently install agent integrations.
124
+ Authorize this device and idempotently install agent integrations.
106
125
 
107
126
  Usage:
127
+ lore connect --login [options]
108
128
  lore connect --token <token> [options]
109
129
 
110
130
  Options:
111
131
  --url <url> Lore API base URL (default: https://api.uselore.co)
112
132
  --dashboard-url <url> Lore dashboard URL (default: https://uselore.co for hosted Lore)
133
+ --login Approve access securely in the Lore dashboard
134
+ --no-open Print the approval URL without opening a browser
113
135
  --token <token> Workspace bearer token (or LORE_WORKSPACE_TOKEN/LORE_TOKEN)
114
- --agent <name> claude, codex, cursor, opencode, polytoken, or t3code; repeat to override auto-detection
136
+ --agent <name> claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, opencode, polytoken, or t3code
115
137
  --timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
116
138
  --json Print machine-readable output
117
139
  --help Show this command's help
118
140
 
119
141
  Examples:
142
+ lore connect --login --agent claude
143
+ lore connect --login --agent cowork
120
144
  lore connect --token "$LORE_WORKSPACE_TOKEN" --agent claude
121
145
  lore connect --url http://localhost:3004 --token dev-token --agent codex
122
146
  lore connect --token "$LORE_WORKSPACE_TOKEN" --agent cursor
@@ -167,6 +191,7 @@ export function getLorePaths(home, environment = process.env) {
167
191
  ? resolve(resolvedHome, ".config")
168
192
  : resolve(environment.XDG_CONFIG_HOME);
169
193
  const t3Home = resolve(environment.T3CODE_HOME?.trim() || resolve(resolvedHome, ".t3"));
194
+ const copilotHome = resolve(environment.COPILOT_HOME?.trim() || resolve(resolvedHome, ".copilot"));
170
195
  return {
171
196
  home: resolvedHome,
172
197
  loreDirectory,
@@ -183,6 +208,9 @@ export function getLorePaths(home, environment = process.env) {
183
208
  state: resolve(loreDirectory, "state"),
184
209
  queue: resolve(loreDirectory, "queue"),
185
210
  codexHooks: resolve(resolvedHome, ".codex", "hooks.json"),
211
+ copilotCliHooks: resolve(copilotHome, "hooks", "lore.json"),
212
+ copilotVscodeHooks: resolve(environment.LORE_COPILOT_VSCODE_HOOKS?.trim() ||
213
+ resolve(process.cwd(), ".github", "hooks", "lore-vscode.json")),
186
214
  claudeSettings: resolve(resolvedHome, ".claude", "settings.json"),
187
215
  cursorHooks: resolve(resolvedHome, ".cursor", "hooks.json"),
188
216
  cursorUserData: platform() === "darwin"
@@ -200,14 +228,18 @@ function isConfiguredAgent(value) {
200
228
  return typeof value === "string" && isNativeCodingAgent(value);
201
229
  }
202
230
  function isConnectAgent(value) {
203
- return isConfiguredAgent(value) || value === "t3code";
231
+ return (isConfiguredAgent(value) ||
232
+ value === "copilot" ||
233
+ value === "cowork" ||
234
+ value === "t3code");
204
235
  }
205
- function hookCommand(agent, paths) {
236
+ function hookCommand(agent, paths, event) {
206
237
  const loreHome = `LORE_HOME=${shellQuote(paths.home)}`;
238
+ const hookEvent = event === undefined ? "" : ` LORE_HOOK_EVENT=${shellQuote(event)}`;
207
239
  if (IS_STANDALONE_BINARY) {
208
- return `env -u BUN_OPTIONS -u BUN_BE_BUN ${loreHome} ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
240
+ return `env -u BUN_OPTIONS -u BUN_BE_BUN ${loreHome}${hookEvent} ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
209
241
  }
210
- return `env ${loreHome} ${shellQuote(process.execPath)} ${shellQuote(paths.runtime)} --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
242
+ return `env ${loreHome}${hookEvent} ${shellQuote(process.execPath)} ${shellQuote(paths.runtime)} --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
211
243
  }
212
244
  function isLoreHook(value) {
213
245
  if (!isObject(value) || value.type !== "command") {
@@ -396,6 +428,169 @@ export function countLoreCursorHooks(input) {
396
428
  return count + hooks[event].filter(isLoreHook).length;
397
429
  }, 0);
398
430
  }
431
+ function copilotCliEventHandler(event, paths) {
432
+ return {
433
+ type: "command",
434
+ bash: hookCommand("copilot-cli", paths, event),
435
+ timeoutSec: event === "sessionEnd"
436
+ ? 2
437
+ : event === "agentStop"
438
+ ? 3
439
+ : event === "preToolUse"
440
+ ? 10
441
+ : 25,
442
+ };
443
+ }
444
+ function isLoreCopilotHook(value) {
445
+ if (!isObject(value) || value.type !== "command") {
446
+ return false;
447
+ }
448
+ const command = typeof value.bash === "string"
449
+ ? value.bash
450
+ : typeof value.command === "string"
451
+ ? value.command
452
+ : "";
453
+ return (/(?:^|\s)--owner(?:=|\s+)lore(?:\s|$)/u.test(command) ||
454
+ command.includes("/.lore/bin/lore-hook.mjs"));
455
+ }
456
+ function stripLoreFromCopilotEvent(value) {
457
+ return Array.isArray(value)
458
+ ? value.filter((hook) => !isLoreCopilotHook(hook))
459
+ : [];
460
+ }
461
+ export function mergeLoreCopilotCliHooks(input, paths) {
462
+ const result = cloneObject(input);
463
+ if (result.version !== undefined && result.version !== 1) {
464
+ throw new Error('Copilot hook configuration field "version" must be 1');
465
+ }
466
+ if (result.hooks !== undefined && !isObject(result.hooks)) {
467
+ throw new Error('Copilot hook configuration field "hooks" must be an object');
468
+ }
469
+ const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
470
+ for (const event of COPILOT_CLI_HOOK_EVENTS) {
471
+ if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
472
+ throw new Error(`Copilot hook event "${event}" must be an array`);
473
+ }
474
+ hooks[event] = [
475
+ ...stripLoreFromCopilotEvent(hooks[event]),
476
+ copilotCliEventHandler(event, paths),
477
+ ];
478
+ }
479
+ result.version = 1;
480
+ result.hooks = hooks;
481
+ return result;
482
+ }
483
+ export function removeLoreCopilotCliHooks(input) {
484
+ const result = cloneObject(input);
485
+ if (!isObject(result.hooks)) {
486
+ return result;
487
+ }
488
+ const hooks = { ...result.hooks };
489
+ for (const event of COPILOT_CLI_HOOK_EVENTS) {
490
+ if (!Array.isArray(hooks[event])) {
491
+ continue;
492
+ }
493
+ const remaining = stripLoreFromCopilotEvent(hooks[event]);
494
+ if (remaining.length === 0) {
495
+ delete hooks[event];
496
+ }
497
+ else {
498
+ hooks[event] = remaining;
499
+ }
500
+ }
501
+ if (Object.keys(hooks).length === 0) {
502
+ delete result.hooks;
503
+ }
504
+ else {
505
+ result.hooks = hooks;
506
+ }
507
+ return result;
508
+ }
509
+ export function countLoreCopilotCliHooks(input) {
510
+ if (!isObject(input.hooks)) {
511
+ return 0;
512
+ }
513
+ return COPILOT_CLI_HOOK_EVENTS.reduce((count, event) => {
514
+ const hooks = input.hooks;
515
+ if (!isObject(hooks) || !Array.isArray(hooks[event])) {
516
+ return count;
517
+ }
518
+ return count + hooks[event].filter(isLoreCopilotHook).length;
519
+ }, 0);
520
+ }
521
+ function copilotVscodeEventHandler(event, paths) {
522
+ return {
523
+ type: "command",
524
+ command: hookCommand("copilot-vscode", paths, event),
525
+ timeout: event === "SessionEnd"
526
+ ? 2
527
+ : event === "Stop"
528
+ ? 3
529
+ : event === "PreToolUse"
530
+ ? 10
531
+ : 25,
532
+ };
533
+ }
534
+ export function mergeLoreCopilotVscodeHooks(input, paths) {
535
+ const result = cloneObject(input);
536
+ if (result.version !== undefined && result.version !== 1) {
537
+ throw new Error('VS Code Copilot hook field "version" must be 1');
538
+ }
539
+ if (result.hooks !== undefined && !isObject(result.hooks)) {
540
+ throw new Error('VS Code Copilot hook field "hooks" must be an object');
541
+ }
542
+ const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
543
+ for (const event of HOOK_EVENTS) {
544
+ if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
545
+ throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
546
+ }
547
+ hooks[event] = [
548
+ ...stripLoreFromCursorEvent(hooks[event]),
549
+ copilotVscodeEventHandler(event, paths),
550
+ ];
551
+ }
552
+ result.version = 1;
553
+ result.hooks = hooks;
554
+ return result;
555
+ }
556
+ export function removeLoreCopilotVscodeHooks(input) {
557
+ const result = cloneObject(input);
558
+ if (!isObject(result.hooks)) {
559
+ return result;
560
+ }
561
+ const hooks = { ...result.hooks };
562
+ for (const event of HOOK_EVENTS) {
563
+ if (!Array.isArray(hooks[event])) {
564
+ continue;
565
+ }
566
+ const remaining = stripLoreFromCursorEvent(hooks[event]);
567
+ if (remaining.length === 0) {
568
+ delete hooks[event];
569
+ }
570
+ else {
571
+ hooks[event] = remaining;
572
+ }
573
+ }
574
+ if (Object.keys(hooks).length === 0) {
575
+ delete result.hooks;
576
+ }
577
+ else {
578
+ result.hooks = hooks;
579
+ }
580
+ return result;
581
+ }
582
+ export function countLoreCopilotVscodeHooks(input) {
583
+ if (!isObject(input.hooks)) {
584
+ return 0;
585
+ }
586
+ return HOOK_EVENTS.reduce((count, event) => {
587
+ const hooks = input.hooks;
588
+ if (!isObject(hooks) || !Array.isArray(hooks[event])) {
589
+ return count;
590
+ }
591
+ return count + hooks[event].filter(isLoreHook).length;
592
+ }, 0);
593
+ }
399
594
  function polytokenEventHandler(event, paths) {
400
595
  return {
401
596
  name: `lore-${event.replaceAll("_", "-")}`,
@@ -536,6 +731,54 @@ function cursorInstallationUnits(input, paths, index, total) {
536
731
  };
537
732
  });
538
733
  }
734
+ function copilotCliInstallationUnits(input, paths, index, total) {
735
+ const configuration = objectConfiguration(input, "copilot-cli");
736
+ if (configuration.version !== undefined && configuration.version !== 1) {
737
+ throw new Error('Copilot hook configuration field "version" must be 1');
738
+ }
739
+ if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
740
+ throw new Error('Copilot hook configuration field "hooks" must be an object');
741
+ }
742
+ const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
743
+ return COPILOT_CLI_HOOK_EVENTS.map((event) => {
744
+ const values = hooks[event];
745
+ if (values !== undefined && !Array.isArray(values)) {
746
+ throw new Error(`Copilot hook event "${event}" must be an array`);
747
+ }
748
+ const candidates = Array.isArray(values) ? values : [];
749
+ const expected = copilotCliEventHandler(event, paths);
750
+ const ownedCount = candidates.filter(isLoreCopilotHook).length;
751
+ const exactCount = candidates.filter((candidate) => exactConfiguration(candidate, expected)).length;
752
+ return {
753
+ id: installationUnitId(event, index, total),
754
+ state: installationUnitState(ownedCount, exactCount),
755
+ };
756
+ });
757
+ }
758
+ function copilotVscodeInstallationUnits(input, paths, index, total) {
759
+ const configuration = objectConfiguration(input, "copilot-vscode");
760
+ if (configuration.version !== undefined && configuration.version !== 1) {
761
+ throw new Error('VS Code Copilot hook field "version" must be 1');
762
+ }
763
+ if (configuration.hooks !== undefined && !isObject(configuration.hooks)) {
764
+ throw new Error('VS Code Copilot hook field "hooks" must be an object');
765
+ }
766
+ const hooks = isObject(configuration.hooks) ? configuration.hooks : {};
767
+ return HOOK_EVENTS.map((event) => {
768
+ const values = hooks[event];
769
+ if (values !== undefined && !Array.isArray(values)) {
770
+ throw new Error(`VS Code Copilot hook event "${event}" must be an array`);
771
+ }
772
+ const candidates = Array.isArray(values) ? values : [];
773
+ const expected = copilotVscodeEventHandler(event, paths);
774
+ const ownedCount = candidates.filter(isLoreHook).length;
775
+ const exactCount = candidates.filter((candidate) => exactConfiguration(candidate, expected)).length;
776
+ return {
777
+ id: installationUnitId(event, index, total),
778
+ state: installationUnitState(ownedCount, exactCount),
779
+ };
780
+ });
781
+ }
539
782
  function polytokenInstallationUnits(input, paths, index, total) {
540
783
  if (!Array.isArray(input)) {
541
784
  throw new Error("Polytoken hooks must contain a JSON array");
@@ -574,6 +817,10 @@ function inspectInstallationUnits(input, agent, paths, index, total) {
574
817
  case "claude":
575
818
  case "codex":
576
819
  return commandHookInstallationUnits(input, agent, paths, index, total);
820
+ case "copilot-cli":
821
+ return copilotCliInstallationUnits(input, paths, index, total);
822
+ case "copilot-vscode":
823
+ return copilotVscodeInstallationUnits(input, paths, index, total);
577
824
  case "cursor":
578
825
  return cursorInstallationUnits(input, paths, index, total);
579
826
  case "polytoken":
@@ -883,6 +1130,30 @@ const AGENT_TARGETS = {
883
1130
  remove: (input) => removeLoreHooks(objectConfiguration(input, "codex")),
884
1131
  count: (input) => countLoreHooks(objectConfiguration(input, "codex")),
885
1132
  },
1133
+ "copilot-cli": {
1134
+ integration: "hooks",
1135
+ expected: COPILOT_CLI_HOOK_EVENTS.length,
1136
+ runtimeRequired: true,
1137
+ emptyValue: () => ({}),
1138
+ configPath: (paths) => paths.copilotCliHooks,
1139
+ detect: async (paths) => (await commandExists("copilot")) || pathExists(paths.copilotCliHooks),
1140
+ executable: async () => commandExists("copilot"),
1141
+ merge: (input, paths) => mergeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli"), paths),
1142
+ remove: (input) => removeLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
1143
+ count: (input) => countLoreCopilotCliHooks(objectConfiguration(input, "copilot-cli")),
1144
+ },
1145
+ "copilot-vscode": {
1146
+ integration: "hooks",
1147
+ expected: HOOK_EVENTS.length,
1148
+ runtimeRequired: true,
1149
+ emptyValue: () => ({}),
1150
+ configPath: (paths) => paths.copilotVscodeHooks,
1151
+ detect: async (paths) => pathExists(paths.copilotVscodeHooks),
1152
+ executable: async () => commandExists("code"),
1153
+ merge: (input, paths) => mergeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode"), paths),
1154
+ remove: (input) => removeLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
1155
+ count: (input) => countLoreCopilotVscodeHooks(objectConfiguration(input, "copilot-vscode")),
1156
+ },
886
1157
  cursor: {
887
1158
  integration: "hooks",
888
1159
  expected: CURSOR_HOOK_EVENTS.length,
@@ -928,7 +1199,10 @@ async function detectAgents(paths) {
928
1199
  agent,
929
1200
  detected: await targetFor(agent).detect(paths),
930
1201
  })));
931
- return detected.flatMap(({ agent, detected: present }) => present ? [agent] : []);
1202
+ const agents = detected.flatMap(({ agent, detected: present }) => present ? [agent] : []);
1203
+ return agents.includes("copilot-cli")
1204
+ ? agents.filter((agent) => agent !== "copilot-vscode")
1205
+ : agents;
932
1206
  }
933
1207
  function installationKey(installation) {
934
1208
  return `${installation.agent}\0${resolve(installation.configPath)}`;
@@ -1088,7 +1362,14 @@ async function readT3CodeProviderInstallations(paths) {
1088
1362
  }
1089
1363
  }
1090
1364
  async function expandConnectInstallations(requested, paths) {
1091
- const direct = requested.flatMap((agent) => agent === "t3code" ? [] : [defaultInstallation(agent, paths)]);
1365
+ const direct = requested.flatMap((agent) => {
1366
+ if (agent === "cowork" || agent === "t3code") {
1367
+ return [];
1368
+ }
1369
+ return [
1370
+ defaultInstallation(agent === "copilot" ? "copilot-cli" : agent, paths),
1371
+ ];
1372
+ });
1092
1373
  if (!requested.includes("t3code")) {
1093
1374
  return { installations: direct, warnings: [] };
1094
1375
  }
@@ -1203,7 +1484,12 @@ function valueAfter(args, index, flag) {
1203
1484
  return [value, index + 1];
1204
1485
  }
1205
1486
  function parseConnectArguments(args) {
1206
- const parsed = { agents: [], json: false };
1487
+ const parsed = {
1488
+ agents: [],
1489
+ json: false,
1490
+ login: false,
1491
+ openBrowser: true,
1492
+ };
1207
1493
  for (let index = 0; index < args.length; index += 1) {
1208
1494
  const argument = args[index];
1209
1495
  if (argument === "--help" || argument === "-h") {
@@ -1214,6 +1500,14 @@ function parseConnectArguments(args) {
1214
1500
  parsed.json = true;
1215
1501
  continue;
1216
1502
  }
1503
+ if (argument === "--login") {
1504
+ parsed.login = true;
1505
+ continue;
1506
+ }
1507
+ if (argument === "--no-open") {
1508
+ parsed.openBrowser = false;
1509
+ continue;
1510
+ }
1217
1511
  if (argument !== "--url" &&
1218
1512
  argument !== "--dashboard-url" &&
1219
1513
  argument !== "--token" &&
@@ -1243,9 +1537,17 @@ function parseConnectArguments(args) {
1243
1537
  parsed.agents.push(value);
1244
1538
  }
1245
1539
  else {
1246
- throw new Error("--agent must be claude, codex, cursor, opencode, polytoken, or t3code");
1540
+ throw new Error("--agent must be claude, codex, copilot, copilot-cli, copilot-vscode, cowork, cursor, opencode, polytoken, or t3code");
1247
1541
  }
1248
1542
  }
1543
+ if (parsed.login && parsed.token !== undefined) {
1544
+ throw new Error("--login and --token cannot be used together");
1545
+ }
1546
+ if ((parsed.agents.includes("copilot-cli") ||
1547
+ parsed.agents.includes("copilot")) &&
1548
+ parsed.agents.includes("copilot-vscode")) {
1549
+ throw new Error("Install either copilot-cli or copilot-vscode in one connection. Both hosts load repository hooks and would invoke Lore twice.");
1550
+ }
1249
1551
  return parsed;
1250
1552
  }
1251
1553
  function parseOutputArguments(args, help, command) {
@@ -1266,38 +1568,26 @@ function parseOutputArguments(args, help, command) {
1266
1568
  function writeResult(value, json, text) {
1267
1569
  process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : text);
1268
1570
  }
1269
- async function connectCommand(args) {
1270
- const parsed = parseConnectArguments(args);
1271
- if (parsed === null) {
1272
- return;
1273
- }
1571
+ export async function connectWithCredential(options) {
1274
1572
  if (platform() !== "darwin" && platform() !== "linux") {
1275
1573
  throw new Error("Lore agent integrations currently support macOS and Linux");
1276
1574
  }
1277
1575
  const paths = getLorePaths();
1278
1576
  const existing = await readConnectorConfig(paths);
1279
- const hasProvidedToken = parsed.token !== undefined ||
1280
- (process.env.LORE_WORKSPACE_TOKEN?.trim() ?? "") !== "" ||
1281
- (process.env.LORE_TOKEN?.trim() ?? "") !== "";
1282
- const { apiUrl, dashboardUrl } = resolveConnectEndpoints({
1283
- ...(parsed.apiUrl === undefined ? {} : { apiUrl: parsed.apiUrl }),
1284
- ...(parsed.dashboardUrl === undefined
1285
- ? {}
1286
- : { dashboardUrl: parsed.dashboardUrl }),
1287
- environment: process.env,
1288
- existing,
1289
- preferHostedDefault: hasProvidedToken,
1290
- });
1291
- const token = configuredToken(parsed.token, existing);
1292
- if (token === undefined || token.trim() === "") {
1293
- throw new Error("Workspace token is required. Use --token <token>, LORE_WORKSPACE_TOKEN, or LORE_TOKEN.");
1577
+ const apiUrl = normalizeApiUrl(options.apiUrl);
1578
+ const dashboardUrl = options.dashboardUrl === undefined
1579
+ ? undefined
1580
+ : normalizeApiUrl(options.dashboardUrl);
1581
+ const token = options.token.trim();
1582
+ if (token === "") {
1583
+ throw new Error("Workspace token cannot be empty");
1294
1584
  }
1295
- const timeoutMs = parsed.timeoutMs ?? existing?.timeoutMs ?? 2_500;
1585
+ const timeoutMs = options.timeoutMs ?? existing?.timeoutMs ?? 2_500;
1296
1586
  const identity = await authenticatedIdentity(apiUrl, token.trim(), timeoutMs);
1297
- const requestedExpansion = parsed.agents.length === 0
1587
+ const requestedExpansion = options.agents.length === 0
1298
1588
  ? { installations: [], warnings: [] }
1299
- : await expandConnectInstallations(parsed.agents, paths);
1300
- const detectedAgents = parsed.agents.length === 0 ? await detectAgents(paths) : [];
1589
+ : await expandConnectInstallations(options.agents, paths);
1590
+ const detectedAgents = options.agents.length === 0 ? await detectAgents(paths) : [];
1301
1591
  const installations = uniqueInstallations([
1302
1592
  ...configuredInstallations(existing, paths),
1303
1593
  ...requestedExpansion.installations,
@@ -1306,8 +1596,9 @@ async function connectCommand(args) {
1306
1596
  const agents = [
1307
1597
  ...new Set(installations.map((installation) => installation.agent)),
1308
1598
  ].sort();
1309
- if (agents.length === 0) {
1310
- throw new Error("No Claude, Codex, Cursor, OpenCode, or Polytoken installation detected. Use --agent <name>; T3 Code users can use --agent t3code.");
1599
+ const credentialOnlyCowork = options.agents.includes("cowork");
1600
+ if (agents.length === 0 && !credentialOnlyCowork) {
1601
+ throw new Error("No Claude, Codex, Copilot CLI, Cursor, OpenCode, or Polytoken installation detected. Use --agent <name>; T3 Code users can use --agent t3code.");
1311
1602
  }
1312
1603
  const now = new Date();
1313
1604
  const documents = new Map();
@@ -1369,17 +1660,154 @@ async function connectCommand(args) {
1369
1660
  await credentialStore.transferPendingTo(reliabilityStore);
1370
1661
  }
1371
1662
  await reliabilityStore.releaseAuthBlocked(now);
1372
- const result = {
1663
+ return {
1373
1664
  connected: true,
1374
1665
  apiUrl: config.apiUrl,
1375
1666
  identity,
1376
- agents,
1667
+ agents: credentialOnlyCowork ? [...agents, "cowork"] : agents,
1377
1668
  config: paths.config,
1378
1669
  changedHookFiles,
1379
1670
  backups,
1380
1671
  warnings: requestedExpansion.warnings,
1381
1672
  };
1382
- writeResult(result, parsed.json, `connected: ${agents.join(", ")}\napi_url: ${config.apiUrl}\nconfig: ${paths.config}\n${requestedExpansion.warnings.map((warning) => `warning: ${warning}\n`).join("")}`);
1673
+ }
1674
+ async function requestDeviceAuthorization(input) {
1675
+ let response;
1676
+ try {
1677
+ response = await fetch(`${input.apiUrl}/v1/device-authorizations`, {
1678
+ method: "POST",
1679
+ headers: {
1680
+ accept: "application/json",
1681
+ "content-type": "application/json",
1682
+ },
1683
+ body: JSON.stringify({
1684
+ clientName: `Lore CLI on ${hostname()}`,
1685
+ agents: input.agents,
1686
+ expiresInDays: 90,
1687
+ }),
1688
+ signal: AbortSignal.timeout(10_000),
1689
+ });
1690
+ }
1691
+ catch {
1692
+ throw new Error("Lore device authorization is unreachable. Verify --url and retry.");
1693
+ }
1694
+ if (!response.ok) {
1695
+ throw new Error(response.status === 404
1696
+ ? "This Lore server does not support browser approval. Upgrade the server or use --token."
1697
+ : "Lore could not start browser approval. Retry in a moment.");
1698
+ }
1699
+ const authorization = CreateDeviceAuthorizationResponseSchema.safeParse(await response.json().catch(() => null));
1700
+ if (!authorization.success) {
1701
+ throw new Error("Lore returned an incompatible device authorization response.");
1702
+ }
1703
+ const serverApprovalUrl = new URL(authorization.data.verificationUriComplete);
1704
+ const approvalUrl = input.dashboardUrl === undefined
1705
+ ? serverApprovalUrl.toString()
1706
+ : new URL(`${serverApprovalUrl.pathname}${serverApprovalUrl.search}`, `${input.dashboardUrl}/`).toString();
1707
+ process.stderr.write(`Approve Lore access with code ${authorization.data.userCode}:\n${approvalUrl}\n`);
1708
+ if (input.openBrowser) {
1709
+ const command = platform() === "darwin" ? "open" : "xdg-open";
1710
+ const child = spawn(command, [approvalUrl], {
1711
+ detached: true,
1712
+ stdio: "ignore",
1713
+ });
1714
+ child.once("error", () => {
1715
+ // The printed URL remains the safe fallback.
1716
+ });
1717
+ child.unref();
1718
+ }
1719
+ const expiresAt = Date.parse(authorization.data.expiresAt);
1720
+ const intervalMs = authorization.data.intervalSeconds * 1_000;
1721
+ while (Date.now() < expiresAt) {
1722
+ await delay(intervalMs);
1723
+ let pollResponse;
1724
+ try {
1725
+ pollResponse = await fetch(`${input.apiUrl}/v1/device-authorizations/poll`, {
1726
+ method: "POST",
1727
+ headers: {
1728
+ accept: "application/json",
1729
+ "content-type": "application/json",
1730
+ },
1731
+ body: JSON.stringify({
1732
+ deviceCode: authorization.data.deviceCode,
1733
+ }),
1734
+ signal: AbortSignal.timeout(10_000),
1735
+ });
1736
+ }
1737
+ catch {
1738
+ continue;
1739
+ }
1740
+ if (pollResponse.status === 429) {
1741
+ const retryAfter = Number(pollResponse.headers.get("retry-after"));
1742
+ if (Number.isFinite(retryAfter) && retryAfter > 0) {
1743
+ await delay(retryAfter * 1_000);
1744
+ }
1745
+ continue;
1746
+ }
1747
+ if (!pollResponse.ok) {
1748
+ throw new Error("Lore browser approval failed. Run connect --login again.");
1749
+ }
1750
+ const result = PollDeviceAuthorizationResponseSchema.safeParse(await pollResponse.json().catch(() => null));
1751
+ if (!result.success) {
1752
+ throw new Error("Lore returned an incompatible device authorization response.");
1753
+ }
1754
+ if (result.data.status === "pending") {
1755
+ continue;
1756
+ }
1757
+ if (result.data.status === "approved") {
1758
+ return result.data.token;
1759
+ }
1760
+ if (result.data.status === "denied") {
1761
+ throw new Error("Lore access was denied in the dashboard.");
1762
+ }
1763
+ throw new Error("Lore browser approval expired. Run connect --login again.");
1764
+ }
1765
+ throw new Error("Lore browser approval expired. Run connect --login again.");
1766
+ }
1767
+ async function connectCommand(args) {
1768
+ const parsed = parseConnectArguments(args);
1769
+ if (parsed === null) {
1770
+ return;
1771
+ }
1772
+ const paths = getLorePaths();
1773
+ const existing = await readConnectorConfig(paths);
1774
+ const hasProvidedToken = parsed.token !== undefined ||
1775
+ (process.env.LORE_WORKSPACE_TOKEN?.trim() ?? "") !== "" ||
1776
+ (process.env.LORE_TOKEN?.trim() ?? "") !== "";
1777
+ const { apiUrl, dashboardUrl } = resolveConnectEndpoints({
1778
+ ...(parsed.apiUrl === undefined ? {} : { apiUrl: parsed.apiUrl }),
1779
+ ...(parsed.dashboardUrl === undefined
1780
+ ? {}
1781
+ : { dashboardUrl: parsed.dashboardUrl }),
1782
+ environment: process.env,
1783
+ existing,
1784
+ preferHostedDefault: parsed.login || hasProvidedToken,
1785
+ });
1786
+ const configured = parsed.login
1787
+ ? undefined
1788
+ : configuredToken(parsed.token, existing);
1789
+ if (parsed.login && hasProvidedToken) {
1790
+ throw new Error("--login cannot be combined with an explicit or environment workspace token. Unset it or use the token directly.");
1791
+ }
1792
+ const token = parsed.login
1793
+ ? await requestDeviceAuthorization({
1794
+ apiUrl,
1795
+ ...(dashboardUrl === undefined ? {} : { dashboardUrl }),
1796
+ agents: parsed.agents,
1797
+ openBrowser: parsed.openBrowser,
1798
+ })
1799
+ : configured;
1800
+ if (token === undefined || token.trim() === "") {
1801
+ throw new Error("Workspace token is required. Use --login, --token <token>, LORE_WORKSPACE_TOKEN, or LORE_TOKEN.");
1802
+ }
1803
+ const result = await connectWithCredential({
1804
+ apiUrl,
1805
+ ...(dashboardUrl === undefined ? {} : { dashboardUrl }),
1806
+ token,
1807
+ agents: parsed.agents,
1808
+ ...(parsed.timeoutMs === undefined ? {} : { timeoutMs: parsed.timeoutMs }),
1809
+ });
1810
+ writeResult(result, parsed.json, `connected: ${result.agents.join(", ")}\napi_url: ${result.apiUrl}\nconfig: ${result.config}\n${result.warnings.map((warning) => `warning: ${warning}\n`).join("")}`);
1383
1811
  }
1384
1812
  async function queueCount(paths) {
1385
1813
  try {
@@ -1665,6 +2093,7 @@ async function statusData(paths) {
1665
2093
  reliability: reliability.inspection,
1666
2094
  reliabilityError: reliability.error,
1667
2095
  agents,
2096
+ externalSurfaces: EXTERNAL_SURFACES,
1668
2097
  };
1669
2098
  }
1670
2099
  async function statusCommand(args) {
@@ -1676,10 +2105,13 @@ async function statusCommand(args) {
1676
2105
  const agentLines = data.agents
1677
2106
  .map((agent) => `${agent.agent}: ${agent.configured ? "configured" : "not configured"}, installation ${agent.installation.state}, invocation ${agent.invocation.state}${agent.invocationError === null ? "" : ` (${agent.invocationError})`}, capture ${agent.capabilities.captureDurability}, guard ${agent.capabilities.guardBoundaryCoverage}/${agent.capabilities.enforcement}`)
1678
2107
  .join("\n");
2108
+ const externalSurfaceLines = data.externalSurfaces
2109
+ .map((surface) => `${surface.surface}: repository template, locally inspectable no, capture ${surface.capabilities.captureDurability}, guard ${surface.capabilities.guardBoundaryCoverage}/${surface.capabilities.enforcement}`)
2110
+ .join("\n");
1679
2111
  const metricRate = (value) => value === null || value === undefined
1680
2112
  ? "-"
1681
2113
  : `${(value * 100).toFixed(1)}%`;
1682
- writeResult(data, parsed.json, `connected: ${data.connected ? "yes" : "no"}\napi_url: ${data.apiUrl ?? "-"}\nconfig_mode: ${data.configMode ?? "-"}\nruntime: ${data.runtimeRequired ? (data.runtimeInstalled ? `installed (${data.runtimeVersion})` : data.runtimeVersion === null ? "missing" : `stale (${data.runtimeVersion})`) : "not required"}\ndurable_outbox: ${data.reliability?.outbox.total ?? 0} ready=${data.reliability?.outbox.ready ?? 0} auth_blocked=${data.reliability?.outbox.authBlocked ?? 0} dead=${data.reliability?.outbox.dead ?? 0}\noldest_capture: ${data.reliability?.outbox.oldestEnqueuedAt ?? "-"}\ncontext_snapshots: ${data.reliability?.cache.context ?? 0}\noldest_context_snapshot: ${data.reliability?.cache.oldestContextWrittenAt ?? "-"}\npolicy_snapshots: ${data.reliability?.cache.policy ?? 0}\nlocal_context_fallback_rate: ${metricRate(data.reliability?.operational.retrieval.fallbackRate)}\nlocal_guard_coverage: ${metricRate(data.reliability?.operational.guard.coverageRate)}\nlocal_guard_failures: ${data.reliability?.operational.guard.failures ?? 0}\nlocal_retrieval_failures: ${data.reliability?.operational.retrieval.failed ?? 0}\n${data.reliabilityError === null ? "" : `reliability_error: ${data.reliabilityError}\n`}${agentLines}\n`);
2114
+ writeResult(data, parsed.json, `connected: ${data.connected ? "yes" : "no"}\napi_url: ${data.apiUrl ?? "-"}\nconfig_mode: ${data.configMode ?? "-"}\nruntime: ${data.runtimeRequired ? (data.runtimeInstalled ? `installed (${data.runtimeVersion})` : data.runtimeVersion === null ? "missing" : `stale (${data.runtimeVersion})`) : "not required"}\ndurable_outbox: ${data.reliability?.outbox.total ?? 0} ready=${data.reliability?.outbox.ready ?? 0} auth_blocked=${data.reliability?.outbox.authBlocked ?? 0} dead=${data.reliability?.outbox.dead ?? 0}\noldest_capture: ${data.reliability?.outbox.oldestEnqueuedAt ?? "-"}\ncontext_snapshots: ${data.reliability?.cache.context ?? 0}\noldest_context_snapshot: ${data.reliability?.cache.oldestContextWrittenAt ?? "-"}\npolicy_snapshots: ${data.reliability?.cache.policy ?? 0}\nlocal_context_fallback_rate: ${metricRate(data.reliability?.operational.retrieval.fallbackRate)}\nlocal_guard_coverage: ${metricRate(data.reliability?.operational.guard.coverageRate)}\nlocal_guard_failures: ${data.reliability?.operational.guard.failures ?? 0}\nlocal_retrieval_failures: ${data.reliability?.operational.retrieval.failed ?? 0}\n${data.reliabilityError === null ? "" : `reliability_error: ${data.reliabilityError}\n`}${agentLines}\n${externalSurfaceLines}\n`);
1683
2115
  }
1684
2116
  async function disconnectCommand(args) {
1685
2117
  const parsed = parseOutputArguments(args, DISCONNECT_HELP, "disconnect");
@@ -1821,6 +2253,19 @@ async function apiChecks(config) {
1821
2253
  }
1822
2254
  return checks;
1823
2255
  }
2256
+ async function verifyConnectedInstallation() {
2257
+ const paths = getLorePaths();
2258
+ const config = await readConnectorConfig(paths);
2259
+ if (config === null) {
2260
+ return false;
2261
+ }
2262
+ const status = await statusData(paths);
2263
+ if (status.configMode !== "600" ||
2264
+ status.agents.some((agent) => agent.configured && agent.installation.state !== "complete")) {
2265
+ return false;
2266
+ }
2267
+ return (await apiChecks(config)).every((check) => check.status !== "error");
2268
+ }
1824
2269
  async function doctorCommand(args) {
1825
2270
  const parsed = parseOutputArguments(args, DOCTOR_HELP, "doctor");
1826
2271
  if (parsed === null) {
@@ -1899,6 +2344,13 @@ async function doctorCommand(args) {
1899
2344
  : "invocation health is unavailable",
1900
2345
  });
1901
2346
  }
2347
+ for (const surface of status.externalSurfaces) {
2348
+ checks.push({
2349
+ name: `${surface.surface}-capability`,
2350
+ status: "ok",
2351
+ detail: "surface-specific repository configuration; local installation is not claimed",
2352
+ });
2353
+ }
1902
2354
  checks.push({
1903
2355
  name: "reliability-store",
1904
2356
  status: status.reliabilityError !== null
@@ -2016,7 +2468,19 @@ export async function runCli(args = process.argv.slice(2)) {
2016
2468
  await doctorCommand(commandArgs);
2017
2469
  return;
2018
2470
  case "self-host":
2019
- await runSelfHostCommand(commandArgs);
2471
+ await runSelfHostCommand(commandArgs, process.env, {
2472
+ connect: async (request) => {
2473
+ const connected = await connectWithCredential({
2474
+ apiUrl: request.apiUrl,
2475
+ token: request.token,
2476
+ agents: request.agents,
2477
+ });
2478
+ return {
2479
+ agents: connected.agents,
2480
+ verified: await verifyConnectedInstallation(),
2481
+ };
2482
+ },
2483
+ });
2020
2484
  return;
2021
2485
  case "demo":
2022
2486
  await runDemoCommand(commandArgs, await readConnectorConfig(getLorePaths()));