@lore-co/cli 0.1.10 → 0.1.12

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
@@ -5,7 +5,7 @@ import { randomUUID } from "node:crypto";
5
5
  import { delimiter, dirname, resolve } from "node:path";
6
6
  import { homedir, platform } from "node:os";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
- import { WorkspaceIdentityResponseSchema, } from "@lore-co/core";
8
+ import { NATIVE_CODING_AGENT_NAMES, WorkspaceIdentityResponseSchema, isNativeCodingAgent, } from "@lore-co/core";
9
9
  import { runHook, } from "./runtime.js";
10
10
  import { connectGithub, runGithubCommand } from "./github.js";
11
11
  import { runDevinCommand } from "./devin.js";
@@ -18,8 +18,17 @@ const LORE_OWNER_ARGUMENT = "--owner lore";
18
18
  export const LORE_OPENCODE_PLUGIN = "@lore-co/opencode";
19
19
  export const DEFAULT_HOSTED_API_URL = "https://api.uselore.co";
20
20
  export const DEFAULT_HOSTED_DASHBOARD_URL = "https://uselore.co";
21
- const CONFIGURED_AGENT_NAMES = ["claude", "codex", "opencode"];
21
+ const CONFIGURED_AGENT_NAMES = NATIVE_CODING_AGENT_NAMES;
22
22
  const HOOK_EVENTS = ["UserPromptSubmit", "Stop", "SessionEnd"];
23
+ const CURSOR_HOOK_EVENTS = [
24
+ "beforeSubmitPrompt",
25
+ "afterAgentResponse",
26
+ "sessionEnd",
27
+ ];
28
+ const POLYTOKEN_HOOK_EVENTS = [
29
+ "pre_user_prompt",
30
+ "post_model_turn",
31
+ ];
23
32
  const ROOT_HELP = `lore
24
33
  Connect local coding agents to Lore shared engineering memory.
25
34
 
@@ -66,7 +75,7 @@ Options:
66
75
  --url <url> Lore API base URL (default: https://api.uselore.co)
67
76
  --dashboard-url <url> Lore dashboard URL (default: https://uselore.co for hosted Lore)
68
77
  --token <token> Workspace bearer token (or LORE_WORKSPACE_TOKEN/LORE_TOKEN)
69
- --agent <name> claude, codex, or opencode; repeat to override auto-detection
78
+ --agent <name> claude, codex, cursor, opencode, polytoken, or t3code; repeat to override auto-detection
70
79
  --timeout-ms <ms> Hook request timeout, 250-10000 (default: 2500)
71
80
  --json Print machine-readable output
72
81
  --help Show this command's help
@@ -74,6 +83,7 @@ Options:
74
83
  Examples:
75
84
  lore connect --token "$LORE_WORKSPACE_TOKEN" --agent claude
76
85
  lore connect --url http://localhost:3004 --token dev-token --agent codex
86
+ lore connect --token "$LORE_WORKSPACE_TOKEN" --agent cursor
77
87
  `;
78
88
  const STATUS_HELP = `lore status
79
89
  Show whether Lore is configured and each native integration is installed.
@@ -112,9 +122,14 @@ function isObject(value) {
112
122
  function cloneObject(value) {
113
123
  return structuredClone(value);
114
124
  }
115
- export function getLorePaths(home) {
116
- const resolvedHome = resolve(home ?? process.env.HOME ?? homedir());
125
+ export function getLorePaths(home, environment = process.env) {
126
+ const resolvedHome = resolve(home ?? environment.HOME ?? homedir());
117
127
  const loreDirectory = resolve(resolvedHome, ".lore");
128
+ const xdgConfigHome = environment.XDG_CONFIG_HOME?.trim() === undefined ||
129
+ environment.XDG_CONFIG_HOME.trim() === ""
130
+ ? resolve(resolvedHome, ".config")
131
+ : resolve(environment.XDG_CONFIG_HOME);
132
+ const t3Home = resolve(environment.T3CODE_HOME?.trim() || resolve(resolvedHome, ".t3"));
118
133
  return {
119
134
  home: resolvedHome,
120
135
  loreDirectory,
@@ -126,23 +141,30 @@ export function getLorePaths(home) {
126
141
  queue: resolve(loreDirectory, "queue"),
127
142
  codexHooks: resolve(resolvedHome, ".codex", "hooks.json"),
128
143
  claudeSettings: resolve(resolvedHome, ".claude", "settings.json"),
144
+ cursorHooks: resolve(resolvedHome, ".cursor", "hooks.json"),
145
+ cursorUserData: platform() === "darwin"
146
+ ? resolve(resolvedHome, "Library", "Application Support", "Cursor")
147
+ : resolve(resolvedHome, ".config", "Cursor"),
129
148
  openCodeConfig: resolve(resolvedHome, ".config", "opencode", "opencode.json"),
149
+ polytokenHooks: resolve(xdgConfigHome, "polytoken", "hooks.json"),
150
+ t3Settings: resolve(t3Home, "userdata", "settings.json"),
130
151
  };
131
152
  }
132
153
  function shellQuote(value) {
133
154
  return `'${value.replaceAll("'", "'\"'\"'")}'`;
134
155
  }
135
156
  function isConfiguredAgent(value) {
136
- return (value === "claude" || value === "codex" || value === "opencode");
157
+ return typeof value === "string" && isNativeCodingAgent(value);
137
158
  }
138
- function isCommandHookAgent(agent) {
139
- return agent === "claude" || agent === "codex";
159
+ function isConnectAgent(value) {
160
+ return isConfiguredAgent(value) || value === "t3code";
140
161
  }
141
162
  function hookCommand(agent, paths) {
163
+ const loreHome = `LORE_HOME=${shellQuote(paths.home)}`;
142
164
  if (IS_STANDALONE_BINARY) {
143
- return `env -u BUN_OPTIONS -u BUN_BE_BUN ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
165
+ return `env -u BUN_OPTIONS -u BUN_BE_BUN ${loreHome} ${shellQuote(process.execPath)} hook --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
144
166
  }
145
- return `${shellQuote(process.execPath)} ${shellQuote(paths.runtime)} --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
167
+ return `env ${loreHome} ${shellQuote(process.execPath)} ${shellQuote(paths.runtime)} --agent ${agent} ${LORE_OWNER_ARGUMENT}`;
146
168
  }
147
169
  function isLoreHook(value) {
148
170
  if (!isObject(value) || value.type !== "command") {
@@ -242,6 +264,113 @@ export function countLoreHooks(input) {
242
264
  }
243
265
  return count;
244
266
  }
267
+ function cursorEventHandler(event, paths) {
268
+ return {
269
+ type: "command",
270
+ command: hookCommand("cursor", paths),
271
+ timeout: event === "sessionEnd" ? 2 : event === "afterAgentResponse" ? 3 : 25,
272
+ };
273
+ }
274
+ function stripLoreFromCursorEvent(value) {
275
+ return Array.isArray(value) ? value.filter((hook) => !isLoreHook(hook)) : [];
276
+ }
277
+ export function mergeLoreCursorHooks(input, paths) {
278
+ const result = cloneObject(input);
279
+ if (result.version !== undefined && result.version !== 1) {
280
+ throw new Error('Cursor configuration field "version" must be 1');
281
+ }
282
+ if (result.hooks !== undefined && !isObject(result.hooks)) {
283
+ throw new Error('Cursor configuration field "hooks" must be a JSON object');
284
+ }
285
+ const hooks = isObject(result.hooks) ? { ...result.hooks } : {};
286
+ for (const event of CURSOR_HOOK_EVENTS) {
287
+ if (hooks[event] !== undefined && !Array.isArray(hooks[event])) {
288
+ throw new Error(`Cursor hook event "${event}" must be a JSON array`);
289
+ }
290
+ hooks[event] = [
291
+ ...stripLoreFromCursorEvent(hooks[event]),
292
+ cursorEventHandler(event, paths),
293
+ ];
294
+ }
295
+ result.version = 1;
296
+ result.hooks = hooks;
297
+ return result;
298
+ }
299
+ export function removeLoreCursorHooks(input) {
300
+ const result = cloneObject(input);
301
+ if (!isObject(result.hooks)) {
302
+ return result;
303
+ }
304
+ const hooks = { ...result.hooks };
305
+ for (const event of CURSOR_HOOK_EVENTS) {
306
+ if (!Array.isArray(hooks[event])) {
307
+ continue;
308
+ }
309
+ const remaining = stripLoreFromCursorEvent(hooks[event]);
310
+ if (remaining.length === 0) {
311
+ delete hooks[event];
312
+ }
313
+ else {
314
+ hooks[event] = remaining;
315
+ }
316
+ }
317
+ if (Object.keys(hooks).length === 0) {
318
+ delete result.hooks;
319
+ }
320
+ else {
321
+ result.hooks = hooks;
322
+ }
323
+ return result;
324
+ }
325
+ export function countLoreCursorHooks(input) {
326
+ if (!isObject(input.hooks)) {
327
+ return 0;
328
+ }
329
+ return CURSOR_HOOK_EVENTS.reduce((count, event) => {
330
+ const hooks = input.hooks;
331
+ if (!isObject(hooks) || !Array.isArray(hooks[event])) {
332
+ return count;
333
+ }
334
+ return count + hooks[event].filter(isLoreHook).length;
335
+ }, 0);
336
+ }
337
+ function polytokenEventHandler(event, paths) {
338
+ return {
339
+ name: `lore-${event.replaceAll("_", "-")}`,
340
+ event,
341
+ handler: { bash: hookCommand("polytoken", paths) },
342
+ };
343
+ }
344
+ function isLorePolytokenHook(value) {
345
+ if (!isObject(value) || !isObject(value.handler)) {
346
+ return false;
347
+ }
348
+ const command = value.handler.bash;
349
+ return (typeof command === "string" &&
350
+ (/(?:^|\s)--owner(?:=|\s+)lore(?:\s|$)/u.test(command) ||
351
+ command.includes("/.lore/bin/lore-hook.mjs")));
352
+ }
353
+ export function mergeLorePolytokenHooks(input, paths) {
354
+ if (!Array.isArray(input)) {
355
+ throw new Error("Polytoken hooks must contain a JSON array");
356
+ }
357
+ const retained = input.filter((hook) => !isLorePolytokenHook(hook));
358
+ return [
359
+ ...structuredClone(retained),
360
+ ...POLYTOKEN_HOOK_EVENTS.map((event) => polytokenEventHandler(event, paths)),
361
+ ];
362
+ }
363
+ export function removeLorePolytokenHooks(input) {
364
+ if (!Array.isArray(input)) {
365
+ throw new Error("Polytoken hooks must contain a JSON array");
366
+ }
367
+ return structuredClone(input.filter((hook) => !isLorePolytokenHook(hook)));
368
+ }
369
+ export function countLorePolytokenHooks(input) {
370
+ return Array.isArray(input)
371
+ ? input.filter((hook) => isLorePolytokenHook(hook)).length
372
+ : 0;
373
+ }
245
374
  function isLoreOpenCodePlugin(value) {
246
375
  return (typeof value === "string" &&
247
376
  /^@lore-co\/(?:opencode|opencode-plugin)(?:@[^\s]+)?$/u.test(value));
@@ -284,9 +413,6 @@ async function readJsonDocument(path) {
284
413
  stat(path),
285
414
  ]);
286
415
  const parsed = JSON.parse(raw);
287
- if (!isObject(parsed)) {
288
- throw new Error(`Expected a JSON object in ${path}`);
289
- }
290
416
  return {
291
417
  exists: true,
292
418
  value: parsed,
@@ -298,7 +424,7 @@ async function readJsonDocument(path) {
298
424
  ? error.code
299
425
  : undefined;
300
426
  if (code === "ENOENT") {
301
- return { exists: false, value: {}, mode: 0o600 };
427
+ return { exists: false, value: undefined, mode: 0o600 };
302
428
  }
303
429
  if (error instanceof SyntaxError) {
304
430
  throw new Error(`Refusing to overwrite invalid JSON in ${path}`, {
@@ -347,6 +473,25 @@ function parseConnectorConfig(value) {
347
473
  return null;
348
474
  }
349
475
  const agents = value.agents.filter((agent) => isConfiguredAgent(agent));
476
+ const parsedAgentConfigPaths = {};
477
+ if (isObject(value.agentConfigPaths)) {
478
+ for (const agent of CONFIGURED_AGENT_NAMES) {
479
+ const paths = value.agentConfigPaths[agent];
480
+ if (!Array.isArray(paths)) {
481
+ continue;
482
+ }
483
+ const valid = paths.flatMap((path) => {
484
+ const normalized = typeof path === "string" ? path.trim() : "";
485
+ return normalized === "" ? [] : [resolve(normalized)];
486
+ });
487
+ if (valid.length > 0) {
488
+ parsedAgentConfigPaths[agent] = [...new Set(valid)];
489
+ }
490
+ }
491
+ }
492
+ const agentConfigPaths = Object.keys(parsedAgentConfigPaths).length === 0
493
+ ? undefined
494
+ : parsedAgentConfigPaths;
350
495
  const timeoutMs = typeof value.timeoutMs === "number" &&
351
496
  Number.isInteger(value.timeoutMs) &&
352
497
  value.timeoutMs >= 250 &&
@@ -362,6 +507,10 @@ function parseConnectorConfig(value) {
362
507
  ...(dashboardUrl === undefined ? {} : { dashboardUrl }),
363
508
  token: value.token,
364
509
  agents,
510
+ ...(agentConfigPaths === undefined ||
511
+ Object.keys(agentConfigPaths).length === 0
512
+ ? {}
513
+ : { agentConfigPaths }),
365
514
  connectedAt: value.connectedAt,
366
515
  timeoutMs,
367
516
  };
@@ -518,20 +667,266 @@ async function pathExists(path) {
518
667
  return false;
519
668
  }
520
669
  }
670
+ async function cursorAvailable(paths) {
671
+ return ((await commandExists("cursor")) ||
672
+ (await pathExists(paths.cursorUserData)));
673
+ }
674
+ function objectConfiguration(input, agent) {
675
+ if (!isObject(input)) {
676
+ throw new Error(`${agent} configuration must contain a JSON object`);
677
+ }
678
+ return input;
679
+ }
680
+ const AGENT_TARGETS = {
681
+ claude: {
682
+ integration: "hooks",
683
+ expected: HOOK_EVENTS.length,
684
+ runtimeRequired: true,
685
+ emptyValue: () => ({}),
686
+ configPath: (paths) => paths.claudeSettings,
687
+ detect: async () => commandExists("claude"),
688
+ executable: async () => commandExists("claude"),
689
+ merge: (input, paths) => mergeLoreHooks(objectConfiguration(input, "claude"), "claude", paths),
690
+ remove: (input) => removeLoreHooks(objectConfiguration(input, "claude")),
691
+ count: (input) => countLoreHooks(objectConfiguration(input, "claude")),
692
+ },
693
+ codex: {
694
+ integration: "hooks",
695
+ expected: HOOK_EVENTS.length,
696
+ runtimeRequired: true,
697
+ emptyValue: () => ({}),
698
+ configPath: (paths) => paths.codexHooks,
699
+ detect: async () => commandExists("codex"),
700
+ executable: async () => commandExists("codex"),
701
+ merge: (input, paths) => mergeLoreHooks(objectConfiguration(input, "codex"), "codex", paths),
702
+ remove: (input) => removeLoreHooks(objectConfiguration(input, "codex")),
703
+ count: (input) => countLoreHooks(objectConfiguration(input, "codex")),
704
+ },
705
+ cursor: {
706
+ integration: "hooks",
707
+ expected: CURSOR_HOOK_EVENTS.length,
708
+ runtimeRequired: true,
709
+ emptyValue: () => ({}),
710
+ configPath: (paths) => paths.cursorHooks,
711
+ detect: async (paths) => (await cursorAvailable(paths)) || pathExists(paths.cursorHooks),
712
+ executable: cursorAvailable,
713
+ merge: (input, paths) => mergeLoreCursorHooks(objectConfiguration(input, "cursor"), paths),
714
+ remove: (input) => removeLoreCursorHooks(objectConfiguration(input, "cursor")),
715
+ count: (input) => countLoreCursorHooks(objectConfiguration(input, "cursor")),
716
+ },
717
+ opencode: {
718
+ integration: "plugin",
719
+ expected: 1,
720
+ runtimeRequired: false,
721
+ emptyValue: () => ({}),
722
+ configPath: (paths) => paths.openCodeConfig,
723
+ detect: async (paths) => (await commandExists("opencode")) || pathExists(paths.openCodeConfig),
724
+ executable: async () => commandExists("opencode"),
725
+ merge: (input) => mergeLoreOpenCodePlugin(objectConfiguration(input, "opencode")),
726
+ remove: (input) => removeLoreOpenCodePlugin(objectConfiguration(input, "opencode")),
727
+ count: (input) => countLoreOpenCodePlugins(objectConfiguration(input, "opencode")),
728
+ },
729
+ polytoken: {
730
+ integration: "hooks",
731
+ expected: POLYTOKEN_HOOK_EVENTS.length,
732
+ runtimeRequired: true,
733
+ emptyValue: () => [],
734
+ configPath: (paths) => paths.polytokenHooks,
735
+ detect: async (paths) => (await commandExists("polytoken")) || pathExists(paths.polytokenHooks),
736
+ executable: async () => commandExists("polytoken"),
737
+ merge: mergeLorePolytokenHooks,
738
+ remove: removeLorePolytokenHooks,
739
+ count: countLorePolytokenHooks,
740
+ },
741
+ };
742
+ function targetFor(agent) {
743
+ return AGENT_TARGETS[agent];
744
+ }
521
745
  async function detectAgents(paths) {
522
- const [codex, claude, openCodeExecutable, openCodeConfig] = await Promise.all([
523
- commandExists("codex"),
524
- commandExists("claude"),
525
- commandExists("opencode"),
526
- pathExists(paths.openCodeConfig),
746
+ const detected = await Promise.all(CONFIGURED_AGENT_NAMES.map(async (agent) => ({
747
+ agent,
748
+ detected: await targetFor(agent).detect(paths),
749
+ })));
750
+ return detected.flatMap(({ agent, detected: present }) => present ? [agent] : []);
751
+ }
752
+ function installationKey(installation) {
753
+ return `${installation.agent}\0${resolve(installation.configPath)}`;
754
+ }
755
+ function uniqueInstallations(installations) {
756
+ const byKey = new Map();
757
+ for (const installation of installations) {
758
+ const normalized = {
759
+ agent: installation.agent,
760
+ configPath: resolve(installation.configPath),
761
+ };
762
+ byKey.set(installationKey(normalized), normalized);
763
+ }
764
+ return [...byKey.values()].sort((left, right) => installationKey(left).localeCompare(installationKey(right)));
765
+ }
766
+ function defaultInstallation(agent, paths) {
767
+ return { agent, configPath: targetFor(agent).configPath(paths) };
768
+ }
769
+ function configuredInstallations(config, paths) {
770
+ return (config?.agents.flatMap((agent) => {
771
+ const configuredPaths = config.agentConfigPaths?.[agent];
772
+ return configuredPaths === undefined
773
+ ? [defaultInstallation(agent, paths)]
774
+ : configuredPaths.map((configPath) => ({ agent, configPath }));
775
+ }) ?? []);
776
+ }
777
+ function nestedObject(input, key) {
778
+ return isObject(input[key]) ? input[key] : {};
779
+ }
780
+ function firstString(...values) {
781
+ for (const value of values) {
782
+ if (typeof value === "string" && value.trim() !== "") {
783
+ return value.trim();
784
+ }
785
+ }
786
+ return undefined;
787
+ }
788
+ function t3ProviderAgent(key, input) {
789
+ const config = nestedObject(input, "config");
790
+ const signal = [
791
+ key,
792
+ firstString(input.driver),
793
+ firstString(input.provider),
794
+ firstString(input.type),
795
+ firstString(config.driver),
796
+ firstString(config.provider),
797
+ firstString(config.type),
798
+ ]
799
+ .filter((value) => value !== undefined)
800
+ .join(" ")
801
+ .toLocaleLowerCase();
802
+ if (signal.includes("opencode")) {
803
+ return "opencode";
804
+ }
805
+ if (signal.includes("cursor")) {
806
+ return "cursor";
807
+ }
808
+ if (signal.includes("claude")) {
809
+ return "claude";
810
+ }
811
+ if (signal.includes("codex")) {
812
+ return "codex";
813
+ }
814
+ return undefined;
815
+ }
816
+ function t3ProviderConfigPath(agent, input, paths) {
817
+ const config = nestedObject(input, "config");
818
+ const environment = nestedObject(input, "environment");
819
+ const configEnvironment = nestedObject(config, "environment");
820
+ const home = nestedObject(input, "home");
821
+ const configHome = nestedObject(config, "home");
822
+ const environmentValue = (key) => environment[key] ?? configEnvironment[key];
823
+ const homePath = firstString(input.homePath, config.homePath, home.path, configHome.path);
824
+ if (agent === "codex") {
825
+ const root = firstString(homePath, environmentValue("CODEX_HOME"));
826
+ return root === undefined
827
+ ? paths.codexHooks
828
+ : resolve(root, "hooks.json");
829
+ }
830
+ if (agent === "claude") {
831
+ const root = firstString(homePath, environmentValue("CLAUDE_CONFIG_DIR"));
832
+ return root === undefined
833
+ ? paths.claudeSettings
834
+ : resolve(root, "settings.json");
835
+ }
836
+ if (agent === "cursor") {
837
+ return paths.cursorHooks;
838
+ }
839
+ const serverUrl = firstString(input.serverUrl, config.serverUrl);
840
+ if (serverUrl !== undefined) {
841
+ return undefined;
842
+ }
843
+ const explicitConfig = firstString(input.configPath, config.configPath, environmentValue("OPENCODE_CONFIG"));
844
+ if (explicitConfig !== undefined) {
845
+ return resolve(explicitConfig);
846
+ }
847
+ const configDirectory = firstString(input.configDirectory, config.configDirectory, environmentValue("OPENCODE_CONFIG_DIR"));
848
+ if (configDirectory !== undefined) {
849
+ return resolve(configDirectory, "opencode.json");
850
+ }
851
+ const xdgConfigHome = firstString(environmentValue("XDG_CONFIG_HOME"));
852
+ return xdgConfigHome === undefined
853
+ ? paths.openCodeConfig
854
+ : resolve(xdgConfigHome, "opencode", "opencode.json");
855
+ }
856
+ function discoverT3CodeProviders(settings, paths) {
857
+ if (!isObject(settings)) {
858
+ throw new Error("T3 Code settings must contain a JSON object");
859
+ }
860
+ const records = [];
861
+ for (const field of ["providerInstances", "providers"]) {
862
+ const collection = settings[field];
863
+ if (!isObject(collection)) {
864
+ continue;
865
+ }
866
+ for (const [key, value] of Object.entries(collection)) {
867
+ if (isObject(value)) {
868
+ records.push([key, value]);
869
+ }
870
+ }
871
+ }
872
+ const warnings = [];
873
+ const installations = uniqueInstallations(records.flatMap(([key, value]) => {
874
+ if (value.enabled === false || value.disabled === true) {
875
+ return [];
876
+ }
877
+ const agent = t3ProviderAgent(key, value);
878
+ if (agent === undefined) {
879
+ return [];
880
+ }
881
+ const configPath = t3ProviderConfigPath(agent, value, paths);
882
+ if (agent === "opencode" && configPath === undefined) {
883
+ warnings.push(`T3 Code provider "${key}" uses an external OpenCode server; install and configure Lore on that server.`);
884
+ }
885
+ return configPath === undefined ? [] : [{ agent, configPath }];
886
+ }));
887
+ return { installations, warnings };
888
+ }
889
+ export function t3CodeProviderInstallations(settings, paths) {
890
+ return discoverT3CodeProviders(settings, paths).installations;
891
+ }
892
+ async function readT3CodeProviderInstallations(paths) {
893
+ try {
894
+ return discoverT3CodeProviders(JSON.parse(await readFile(paths.t3Settings, "utf8")), paths);
895
+ }
896
+ catch (error) {
897
+ const code = typeof error === "object" && error !== null && "code" in error
898
+ ? error.code
899
+ : undefined;
900
+ if (code === "ENOENT") {
901
+ return { installations: [], warnings: [] };
902
+ }
903
+ if (error instanceof SyntaxError) {
904
+ throw new Error(`Refusing to use invalid T3 Code settings in ${paths.t3Settings}`, { cause: error });
905
+ }
906
+ throw error;
907
+ }
908
+ }
909
+ async function expandConnectInstallations(requested, paths) {
910
+ const direct = requested.flatMap((agent) => agent === "t3code" ? [] : [defaultInstallation(agent, paths)]);
911
+ if (!requested.includes("t3code")) {
912
+ return { installations: direct, warnings: [] };
913
+ }
914
+ const configured = await readT3CodeProviderInstallations(paths);
915
+ const t3Providers = ["claude", "codex", "cursor", "opencode"];
916
+ const detected = await Promise.all(t3Providers.map(async (agent) => ({
917
+ agent,
918
+ available: await targetFor(agent).detect(paths),
919
+ })));
920
+ const routed = detected.flatMap(({ agent, available }) => available ? [defaultInstallation(agent, paths)] : []);
921
+ const installations = uniqueInstallations([
922
+ ...direct,
923
+ ...configured.installations,
924
+ ...routed,
527
925
  ]);
528
- return [
529
- ...(codex ? ["codex"] : []),
530
- ...(claude ? ["claude"] : []),
531
- ...(openCodeExecutable || openCodeConfig
532
- ? ["opencode"]
533
- : []),
534
- ];
926
+ if (installations.length === 0) {
927
+ throw new Error(`T3 Code delegates to provider CLIs, but no supported local Claude, Codex, Cursor, or OpenCode provider was found in ${paths.t3Settings}. Install a provider, set T3CODE_HOME, or connect it explicitly.`);
928
+ }
929
+ return { installations, warnings: configured.warnings };
535
930
  }
536
931
  async function installRuntime(paths) {
537
932
  if (IS_STANDALONE_BINARY) {
@@ -553,28 +948,19 @@ async function installRuntime(paths) {
553
948
  atomicWrite(paths.runtimePackage, '{"type":"module"}\n', 0o600),
554
949
  ]);
555
950
  }
556
- function hookPath(agent, paths) {
557
- return agent === "codex" ? paths.codexHooks : paths.claudeSettings;
558
- }
559
951
  function agentConfigPath(agent, paths) {
560
- return agent === "opencode"
561
- ? paths.openCodeConfig
562
- : hookPath(agent, paths);
952
+ return targetFor(agent).configPath(paths);
563
953
  }
564
954
  function mergeAgentConfig(input, agent, paths) {
565
- return agent === "opencode"
566
- ? mergeLoreOpenCodePlugin(input)
567
- : mergeLoreHooks(input, agent, paths);
955
+ const target = targetFor(agent);
956
+ return target.merge(input === undefined ? target.emptyValue() : input, paths);
568
957
  }
569
958
  function removeAgentConfig(input, agent) {
570
- return agent === "opencode"
571
- ? removeLoreOpenCodePlugin(input)
572
- : removeLoreHooks(input);
959
+ return targetFor(agent).remove(input);
573
960
  }
574
961
  function countAgentIntegrations(input, agent) {
575
- return agent === "opencode"
576
- ? countLoreOpenCodePlugins(input)
577
- : countLoreHooks(input);
962
+ const target = targetFor(agent);
963
+ return target.count(input === undefined ? target.emptyValue() : input);
578
964
  }
579
965
  function parseInteger(value, flag) {
580
966
  const parsed = Number(value);
@@ -627,11 +1013,11 @@ function parseConnectArguments(args) {
627
1013
  }
628
1014
  parsed.timeoutMs = timeoutMs;
629
1015
  }
630
- else if (isConfiguredAgent(value)) {
1016
+ else if (isConnectAgent(value)) {
631
1017
  parsed.agents.push(value);
632
1018
  }
633
1019
  else {
634
- throw new Error("--agent must be claude, codex, or opencode");
1020
+ throw new Error("--agent must be claude, codex, cursor, opencode, polytoken, or t3code");
635
1021
  }
636
1022
  }
637
1023
  return parsed;
@@ -682,50 +1068,63 @@ async function connectCommand(args) {
682
1068
  }
683
1069
  const timeoutMs = parsed.timeoutMs ?? existing?.timeoutMs ?? 2_500;
684
1070
  const identity = await authenticatedIdentity(apiUrl, token.trim(), timeoutMs);
685
- const detected = parsed.agents.length === 0 ? await detectAgents(paths) : [];
1071
+ const requestedExpansion = parsed.agents.length === 0
1072
+ ? { installations: [], warnings: [] }
1073
+ : await expandConnectInstallations(parsed.agents, paths);
1074
+ const detectedAgents = parsed.agents.length === 0 ? await detectAgents(paths) : [];
1075
+ const installations = uniqueInstallations([
1076
+ ...configuredInstallations(existing, paths),
1077
+ ...requestedExpansion.installations,
1078
+ ...detectedAgents.map((agent) => defaultInstallation(agent, paths)),
1079
+ ]);
686
1080
  const agents = [
687
- ...new Set([
688
- ...(existing?.agents ?? []),
689
- ...parsed.agents,
690
- ...detected,
691
- ]),
1081
+ ...new Set(installations.map((installation) => installation.agent)),
692
1082
  ].sort();
693
1083
  if (agents.length === 0) {
694
- throw new Error("No Claude, Codex, or OpenCode installation detected. Use --agent <name>.");
1084
+ throw new Error("No Claude, Codex, Cursor, OpenCode, or Polytoken installation detected. Use --agent <name>; T3 Code users can use --agent t3code.");
695
1085
  }
696
1086
  const now = new Date();
697
1087
  const documents = new Map();
698
1088
  const mergedDocuments = new Map();
699
- for (const agent of agents) {
700
- const document = await readJsonDocument(agentConfigPath(agent, paths));
701
- documents.set(agent, document);
702
- mergedDocuments.set(agent, mergeAgentConfig(document.value, agent, paths));
1089
+ for (const installation of installations) {
1090
+ const key = installationKey(installation);
1091
+ const document = await readJsonDocument(installation.configPath);
1092
+ documents.set(key, document);
1093
+ mergedDocuments.set(key, mergeAgentConfig(document.value, installation.agent, paths));
703
1094
  }
704
- if (agents.some(isCommandHookAgent)) {
1095
+ if (installations.some((installation) => targetFor(installation.agent).runtimeRequired)) {
705
1096
  await installRuntime(paths);
706
1097
  }
707
1098
  const changedHookFiles = [];
708
1099
  const backups = [];
709
- for (const agent of agents) {
710
- const document = documents.get(agent);
711
- const merged = mergedDocuments.get(agent);
1100
+ for (const installation of installations) {
1101
+ const key = installationKey(installation);
1102
+ const document = documents.get(key);
1103
+ const merged = mergedDocuments.get(key);
712
1104
  if (document === undefined || merged === undefined) {
713
1105
  continue;
714
1106
  }
715
- const result = await writeMergedJson(agentConfigPath(agent, paths), document, merged, now);
1107
+ const result = await writeMergedJson(installation.configPath, document, merged, now);
716
1108
  if (result.changed) {
717
- changedHookFiles.push(agentConfigPath(agent, paths));
1109
+ changedHookFiles.push(installation.configPath);
718
1110
  }
719
1111
  if (result.backup !== undefined) {
720
1112
  backups.push(result.backup);
721
1113
  }
722
1114
  }
1115
+ const agentConfigPaths = {};
1116
+ for (const agent of agents) {
1117
+ agentConfigPaths[agent] = installations
1118
+ .filter((installation) => installation.agent === agent)
1119
+ .map((installation) => installation.configPath);
1120
+ }
723
1121
  const config = {
724
1122
  version: 1,
725
1123
  apiUrl,
726
1124
  ...(dashboardUrl === undefined ? {} : { dashboardUrl }),
727
1125
  token: token.trim(),
728
1126
  agents,
1127
+ agentConfigPaths,
729
1128
  connectedAt: existing?.connectedAt ?? now.toISOString(),
730
1129
  timeoutMs,
731
1130
  };
@@ -738,8 +1137,9 @@ async function connectCommand(args) {
738
1137
  config: paths.config,
739
1138
  changedHookFiles,
740
1139
  backups,
1140
+ warnings: requestedExpansion.warnings,
741
1141
  };
742
- writeResult(result, parsed.json, `connected: ${agents.join(", ")}\napi_url: ${config.apiUrl}\nconfig: ${paths.config}\n`);
1142
+ writeResult(result, parsed.json, `connected: ${agents.join(", ")}\napi_url: ${config.apiUrl}\nconfig: ${paths.config}\n${requestedExpansion.warnings.map((warning) => `warning: ${warning}\n`).join("")}`);
743
1143
  }
744
1144
  async function queueCount(paths) {
745
1145
  try {
@@ -750,23 +1150,33 @@ async function queueCount(paths) {
750
1150
  }
751
1151
  }
752
1152
  async function getAgentStatus(agent, config, paths) {
753
- const path = agentConfigPath(agent, paths);
1153
+ const target = targetFor(agent);
1154
+ const configuredPaths = config?.agentConfigPaths?.[agent];
1155
+ const configPaths = config?.agents.includes(agent) === true && configuredPaths !== undefined
1156
+ ? configuredPaths
1157
+ : [agentConfigPath(agent, paths)];
754
1158
  let installed = 0;
755
- try {
756
- installed = countAgentIntegrations((await readJsonDocument(path)).value, agent);
757
- }
758
- catch {
759
- installed = 0;
1159
+ let existing = 0;
1160
+ for (const path of configPaths) {
1161
+ try {
1162
+ const document = await readJsonDocument(path);
1163
+ installed += countAgentIntegrations(document.value, agent);
1164
+ existing += document.exists ? 1 : 0;
1165
+ }
1166
+ catch {
1167
+ // A malformed target config is reported as missing integration state.
1168
+ }
760
1169
  }
761
1170
  return {
762
1171
  agent,
763
1172
  configured: config?.agents.includes(agent) ?? false,
764
- executable: await commandExists(agent),
765
- configExists: await pathExists(path),
766
- configFile: path,
767
- integration: agent === "opencode" ? "plugin" : "hooks",
1173
+ executable: await target.executable(paths),
1174
+ configExists: existing === configPaths.length,
1175
+ configFile: configPaths[0] ?? agentConfigPath(agent, paths),
1176
+ ...(configPaths.length > 1 ? { configFiles: configPaths } : {}),
1177
+ integration: target.integration,
768
1178
  installed,
769
- expected: agent === "opencode" ? 1 : HOOK_EVENTS.length,
1179
+ expected: target.expected * configPaths.length,
770
1180
  };
771
1181
  }
772
1182
  async function statusData(paths) {
@@ -778,7 +1188,7 @@ async function statusData(paths) {
778
1188
  catch {
779
1189
  // Missing configuration is represented as disconnected.
780
1190
  }
781
- const runtimeRequired = config?.agents.some(isCommandHookAgent) ?? false;
1191
+ const runtimeRequired = config?.agents.some((agent) => targetFor(agent).runtimeRequired) ?? false;
782
1192
  const runtimeInstalledCheck = runtimeRequired
783
1193
  ? Promise.all(IS_STANDALONE_BINARY
784
1194
  ? [access(process.execPath, fsConstants.R_OK | fsConstants.X_OK)]
@@ -788,12 +1198,10 @@ async function statusData(paths) {
788
1198
  access(paths.runtimePackage, fsConstants.R_OK),
789
1199
  ]).then(() => true, () => false)
790
1200
  : Promise.resolve(false);
791
- const [runtimeInstalled, queuedTurns, claude, codex, opencode] = await Promise.all([
1201
+ const [runtimeInstalled, queuedTurns, agents] = await Promise.all([
792
1202
  runtimeInstalledCheck,
793
1203
  queueCount(paths),
794
- getAgentStatus("claude", config, paths),
795
- getAgentStatus("codex", config, paths),
796
- getAgentStatus("opencode", config, paths),
1204
+ Promise.all(CONFIGURED_AGENT_NAMES.map(async (agent) => getAgentStatus(agent, config, paths))),
797
1205
  ]);
798
1206
  return {
799
1207
  connected: config !== null,
@@ -803,7 +1211,7 @@ async function statusData(paths) {
803
1211
  runtimeRequired,
804
1212
  runtimeInstalled,
805
1213
  queuedTurns,
806
- agents: [claude, codex, opencode],
1214
+ agents,
807
1215
  };
808
1216
  }
809
1217
  async function statusCommand(args) {
@@ -823,16 +1231,21 @@ async function disconnectCommand(args) {
823
1231
  return;
824
1232
  }
825
1233
  const paths = getLorePaths();
1234
+ const config = await readConnectorConfig(paths);
826
1235
  const now = new Date();
827
1236
  const changedHookFiles = [];
828
1237
  const backups = [];
829
- for (const agent of CONFIGURED_AGENT_NAMES) {
830
- const path = agentConfigPath(agent, paths);
1238
+ const installations = uniqueInstallations([
1239
+ ...CONFIGURED_AGENT_NAMES.map((agent) => defaultInstallation(agent, paths)),
1240
+ ...configuredInstallations(config, paths),
1241
+ ]);
1242
+ for (const installation of installations) {
1243
+ const path = installation.configPath;
831
1244
  const document = await readJsonDocument(path);
832
1245
  if (!document.exists) {
833
1246
  continue;
834
1247
  }
835
- const result = await writeMergedJson(path, document, removeAgentConfig(document.value, agent), now);
1248
+ const result = await writeMergedJson(path, document, removeAgentConfig(document.value, installation.agent), now);
836
1249
  if (result.changed) {
837
1250
  changedHookFiles.push(path);
838
1251
  }
@@ -952,7 +1365,7 @@ async function doctorCommand(args) {
952
1365
  checks.push({
953
1366
  name: `${agent.agent}-executable`,
954
1367
  status: agent.executable ? "ok" : "warning",
955
- detail: agent.executable ? "found on PATH" : "not found on PATH",
1368
+ detail: agent.executable ? "detected" : "not detected",
956
1369
  });
957
1370
  checks.push({
958
1371
  name: `${agent.agent}-${agent.integration}`,