@undefineds.co/linx 0.2.17 → 0.2.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.
Files changed (117) hide show
  1. package/README.md +3 -6
  2. package/dist/generated/version.js +1 -1
  3. package/dist/generated/version.js.map +1 -1
  4. package/dist/index.js +256 -143
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/account-api.js +1 -1
  7. package/dist/lib/account-api.js.map +1 -1
  8. package/dist/lib/account-session.js +1 -1
  9. package/dist/lib/account-session.js.map +1 -1
  10. package/dist/lib/ai-command.js +186 -367
  11. package/dist/lib/ai-command.js.map +1 -1
  12. package/dist/lib/chat-api.js +177 -19
  13. package/dist/lib/chat-api.js.map +1 -1
  14. package/dist/lib/codex-plugin/bridge.js.map +1 -1
  15. package/dist/lib/codex-plugin/codex-native-proxy.js.map +1 -1
  16. package/dist/lib/codex-plugin/index.js.map +1 -1
  17. package/dist/lib/codex-plugin/runner.js.map +1 -1
  18. package/dist/lib/credentials-store.js +7 -1
  19. package/dist/lib/credentials-store.js.map +1 -1
  20. package/dist/lib/default-model.js +1 -0
  21. package/dist/lib/default-model.js.map +1 -1
  22. package/dist/lib/login-command.js +33 -10
  23. package/dist/lib/login-command.js.map +1 -1
  24. package/dist/lib/models.js +2 -27
  25. package/dist/lib/models.js.map +1 -1
  26. package/dist/lib/node-warning-filter.js +34 -0
  27. package/dist/lib/node-warning-filter.js.map +1 -0
  28. package/dist/lib/oidc-auth.js +130 -18
  29. package/dist/lib/oidc-auth.js.map +1 -1
  30. package/dist/lib/oidc-session-storage.js.map +1 -1
  31. package/dist/lib/pi-adapter/auth.js +47 -11
  32. package/dist/lib/pi-adapter/auth.js.map +1 -1
  33. package/dist/lib/pi-adapter/branding.js +802 -78
  34. package/dist/lib/pi-adapter/branding.js.map +1 -1
  35. package/dist/lib/pi-adapter/index.js.map +1 -1
  36. package/dist/lib/pi-adapter/interactive.js +179 -5
  37. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  38. package/dist/lib/pi-adapter/pod-approval.js +8 -0
  39. package/dist/lib/pi-adapter/pod-approval.js.map +1 -0
  40. package/dist/lib/pi-adapter/pod-mirror-mapping.js +189 -0
  41. package/dist/lib/pi-adapter/pod-mirror-mapping.js.map +1 -0
  42. package/dist/lib/pi-adapter/pod-mirror.js +416 -0
  43. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -0
  44. package/dist/lib/pi-adapter/pod-tools.js +104 -0
  45. package/dist/lib/pi-adapter/pod-tools.js.map +1 -0
  46. package/dist/lib/pi-adapter/runtime.js +327 -28
  47. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  48. package/dist/lib/pi-adapter/session.js +608 -0
  49. package/dist/lib/pi-adapter/session.js.map +1 -0
  50. package/dist/lib/pi-adapter/stream.js +154 -4
  51. package/dist/lib/pi-adapter/stream.js.map +1 -1
  52. package/dist/lib/pi-adapter/theme.js.map +1 -1
  53. package/dist/lib/pi-adapter/web-fetch.js +154 -0
  54. package/dist/lib/pi-adapter/web-fetch.js.map +1 -0
  55. package/dist/lib/pod-chat-store.js +40 -30
  56. package/dist/lib/pod-chat-store.js.map +1 -1
  57. package/dist/lib/pod-data-session.js +110 -0
  58. package/dist/lib/pod-data-session.js.map +1 -0
  59. package/dist/lib/profile-identity.js +16 -60
  60. package/dist/lib/profile-identity.js.map +1 -1
  61. package/dist/lib/prompt.js.map +1 -1
  62. package/dist/lib/runtime-target.js +1 -1
  63. package/dist/lib/runtime-target.js.map +1 -1
  64. package/dist/lib/solid-auth.js.map +1 -1
  65. package/dist/lib/thread-utils.js.map +1 -1
  66. package/dist/lib/watch/archive.js +1 -1
  67. package/dist/lib/watch/archive.js.map +1 -1
  68. package/dist/lib/watch/auth.js +1 -1
  69. package/dist/lib/watch/auth.js.map +1 -1
  70. package/dist/lib/watch/codex-composer.js.map +1 -1
  71. package/dist/lib/watch/codex-footer.js.map +1 -1
  72. package/dist/lib/watch/codex-overlay.js.map +1 -1
  73. package/dist/lib/watch/codex-request-form.js +1 -1
  74. package/dist/lib/watch/codex-request-form.js.map +1 -1
  75. package/dist/lib/watch/codex-request-input.js.map +1 -1
  76. package/dist/lib/watch/display.js.map +1 -1
  77. package/dist/lib/watch/format.js.map +1 -1
  78. package/dist/lib/watch/hooks/claude.js +4 -0
  79. package/dist/lib/watch/hooks/claude.js.map +1 -1
  80. package/dist/lib/watch/hooks/codebuddy.js +4 -0
  81. package/dist/lib/watch/hooks/codebuddy.js.map +1 -1
  82. package/dist/lib/watch/hooks/codex.js +4 -0
  83. package/dist/lib/watch/hooks/codex.js.map +1 -1
  84. package/dist/lib/watch/hooks/index.js.map +1 -1
  85. package/dist/lib/watch/hooks/shared.js +0 -1
  86. package/dist/lib/watch/hooks/shared.js.map +1 -1
  87. package/dist/lib/watch/index.js.map +1 -1
  88. package/dist/lib/watch/pod-ai.js +29 -37
  89. package/dist/lib/watch/pod-ai.js.map +1 -1
  90. package/dist/lib/watch/pod-approval.js +822 -220
  91. package/dist/lib/watch/pod-approval.js.map +1 -1
  92. package/dist/lib/watch/pod-persistence.js +214 -106
  93. package/dist/lib/watch/pod-persistence.js.map +1 -1
  94. package/dist/lib/watch/runner.js +243 -38
  95. package/dist/lib/watch/runner.js.map +1 -1
  96. package/dist/lib/watch/secretary.js +238 -0
  97. package/dist/lib/watch/secretary.js.map +1 -0
  98. package/dist/lib/watch/types.js.map +1 -1
  99. package/dist/watch-cli.js +8 -34
  100. package/dist/watch-cli.js.map +1 -1
  101. package/package.json +3 -9
  102. package/vendor/agent-runtime/dist/acp.d.ts +27 -0
  103. package/vendor/agent-runtime/dist/acp.js +86 -0
  104. package/vendor/agent-runtime/dist/companion-model.d.ts +7 -0
  105. package/vendor/agent-runtime/dist/companion-model.js +12 -0
  106. package/vendor/agent-runtime/dist/index.d.ts +3 -0
  107. package/vendor/agent-runtime/dist/index.js +3 -0
  108. package/vendor/agent-runtime/dist/turn-controller.d.ts +69 -0
  109. package/vendor/agent-runtime/dist/turn-controller.js +129 -0
  110. package/vendor/agent-runtime/package.json +11 -0
  111. package/vendor/client/dist/client/index.d.ts +0 -118
  112. package/vendor/client/dist/client/index.js +0 -260
  113. package/vendor/client/dist/index.d.ts +0 -1
  114. package/vendor/client/dist/index.js +0 -1
  115. package/vendor/client/dist/watch/index.d.ts +0 -226
  116. package/vendor/client/dist/watch/index.js +0 -1114
  117. package/vendor/client/package.json +0 -9
package/README.md CHANGED
@@ -44,9 +44,7 @@ yarn workspace @undefineds.co/linx dev watch run claude "先总结这个目录
44
44
  yarn workspace @undefineds.co/linx dev watch run codebuddy -- --tools Read,Edit
45
45
  yarn workspace @undefineds.co/linx dev watch backends
46
46
  yarn workspace @undefineds.co/linx dev watch sessions
47
- yarn workspace @undefineds.co/linx dev watch approvals
48
- yarn workspace @undefineds.co/linx dev watch approve <approvalId> --session
49
- yarn workspace @undefineds.co/linx dev watch reject <approvalId> --reason "unsafe command"
47
+ yarn workspace @undefineds.co/linx dev watch show <sessionId>
50
48
  ```
51
49
 
52
50
  ## Slash Commands
@@ -72,9 +70,8 @@ yarn workspace @undefineds.co/linx dev watch reject <approvalId> --reason "unsaf
72
70
  - `--credential-source local|cloud|auto` 只决定凭据来源;`watch` 当前运行时始终是本地,不会因为选 cloud credential source 就切成 cloud runtime
73
71
  - `--credential-source cloud` 当前可显式用于 `codex` / `claude` / `codebuddy`,前提是对应 API key 已写进 Pod
74
72
  - 单本地会话时,approval 主路径是在当前 watch TUI 内直接处理;不会依赖额外的 approval inbox
75
- - 默认人工审批同时支持当前本地 watch 和 Pod 远端控制面,谁先决策谁生效
76
- - 如果本地已 `linx login`,LinX 会把 pending approval 写进 Pod 的 `approval / audit / inbox_notification`
77
- - `linx watch approvals` / `approve` / `reject` 主要用于远端、后台或多会话场景的 approval inbox;不是本地单会话 watch 的主交互路径
73
+ - 默认人工审批同时支持当前本地 watch 和 Pod 远端控制面,谁先决策谁生效;低上下文的 CLI approval inbox 命令已移除,远端审批队列由 App/Inbox 承载
74
+ - 如果本地已 `linx login`,LinX 会把 pending approval 写进 Pod 的 `approval / audit / inbox_notification`,供 App/Inbox 读取和处理
78
75
  - 当前是最小多轮版:本地 REPL、统一 ACP 会话、归档结构化事件
79
76
  - 在交互式 TTY 里,`watch run` 会默认进入全屏 TUI;非 TTY / 管道输出会自动降级到 plain mode
80
77
  - `linx watch show <sessionId>` 现在会回放归档 timeline,而不是直接输出 `session.json`
@@ -1,3 +1,3 @@
1
1
  // Generated by apps/cli/scripts/build.mjs. Keep a committed fallback for direct test compiles.
2
- export const LINX_CLI_VERSION = "0.2.17";
2
+ export const LINX_CLI_VERSION = "0.2.18";
3
3
  //# sourceMappingURL=version.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.js","sourceRoot":"","sources":["../../../../../src/generated/version.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,MAAM,CAAC,MAAM,gBAAgB,GAAG,QAAQ,CAAA"}
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/generated/version.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,MAAM,CAAC,MAAM,gBAAgB,GAAG,QAAQ,CAAA"}
package/dist/index.js CHANGED
@@ -1,21 +1,46 @@
1
1
  #!/usr/bin/env node
2
+ import './lib/node-warning-filter.js';
3
+ import { readFileSync } from 'node:fs';
2
4
  import yargs from 'yargs';
3
5
  import { hideBin } from 'yargs/helpers';
4
6
  import { aiCommand } from './lib/ai-command.js';
5
7
  import { resolveAccountBaseUrl } from './lib/account-api.js';
6
- import { getClientCredentials, loadCredentials } from './lib/credentials-store.js';
7
- import { loadAccountSession } from './lib/account-session.js';
8
+ import { loadCredentials } from './lib/credentials-store.js';
8
9
  import { loginCommand, logoutCommand, whoamiCommand } from './lib/login-command.js';
9
- import { runPrintMode } from '@mariozechner/pi-coding-agent';
10
+ import { DefaultPackageManager, SettingsManager, runPrintMode } from '@mariozechner/pi-coding-agent';
10
11
  import { promptText } from './lib/prompt.js';
11
12
  import { resolveRuntimeTarget } from './lib/runtime-target.js';
12
13
  import { createCodexNativeProxy } from './lib/codex-plugin/index.js';
13
- import { bootstrapPiInteractiveMode, createPiRuntimeAdapter } from './lib/pi-adapter/index.js';
14
- import { getOidcAccessToken } from './lib/oidc-auth.js';
15
- import { DEFAULT_LINX_CLOUD_MODEL_ID } from './lib/default-model.js';
14
+ import { bootstrapPiInteractiveMode, createPiRuntimeAdapter, resolveLinxInteractiveLoginReason, resolveLinxStartupLoginPromptDecision } from './lib/pi-adapter/index.js';
15
+ import { isOidcLoginExpiredError } from './lib/oidc-auth.js';
16
+ import { createPodDataSession } from './lib/pod-data-session.js';
17
+ import { DEFAULT_LINX_CLOUD_MODEL_ID, FALLBACK_LINX_CLOUD_MODEL_IDS } from './lib/default-model.js';
18
+ import { createLinxPiSessionManager, formatLinxPiSessionSummary, listLinxPiSessions, } from './lib/pi-adapter/session.js';
19
+ import { LinxPiPodMirror } from './lib/pi-adapter/pod-mirror.js';
16
20
  import { LINX_AGENT_DIR } from './lib/pi-adapter/branding.js';
17
- import { LINX_CLI_VERSION } from './generated/version.js';
18
- import { formatRemoteWatchApprovalSummary, formatArchivedWatchSession, formatWatchSessionSummary, loadArchivedWatchEvents, listArchivedWatchSessions, listRemoteWatchApprovals, listSupportedWatchBackends, loadArchivedWatchSession, resolveRemoteWatchApproval, runWatch, } from './lib/watch/index.js';
21
+ import { formatArchivedWatchSession, formatWatchSessionSummary, loadArchivedWatchEvents, listArchivedWatchSessions, listSupportedWatchBackends, loadArchivedWatchSession, runWatch, } from './lib/watch/index.js';
22
+ function readPackageVersion() {
23
+ try {
24
+ const raw = readFileSync(new URL('../package.json', import.meta.url), 'utf-8');
25
+ const pkg = JSON.parse(raw);
26
+ return typeof pkg.version === 'string' && pkg.version.trim() ? pkg.version.trim() : 'unknown';
27
+ }
28
+ catch {
29
+ return 'unknown';
30
+ }
31
+ }
32
+ function formatRemoteModelMetadata(model) {
33
+ const provider = resolveRemoteModelProviderLabel(model);
34
+ return [provider, model.contextWindow ? `${model.contextWindow}` : '']
35
+ .filter(Boolean)
36
+ .join(' · ');
37
+ }
38
+ function resolveRemoteModelProviderLabel(model) {
39
+ if (FALLBACK_LINX_CLOUD_MODEL_IDS.includes(model.id)) {
40
+ return 'undefineds';
41
+ }
42
+ return model.provider || model.ownedBy;
43
+ }
19
44
  let chatRuntimePromise = null;
20
45
  async function loadChatRuntime() {
21
46
  if (!chatRuntimePromise) {
@@ -45,71 +70,53 @@ async function loadChatRuntime() {
45
70
  }
46
71
  async function resolveContext(urlOverride) {
47
72
  const runtime = await loadChatRuntime();
48
- const creds = loadCredentials();
49
- if (!creds) {
50
- throw new Error('No credentials found. Run `linx login` first.');
51
- }
73
+ const podSession = await createLinxPodDataSession();
52
74
  const target = resolveRuntimeTarget({
53
- issuerUrl: creds.url,
75
+ issuerUrl: podSession.credentials.url,
54
76
  runtimeUrlOverride: urlOverride,
55
77
  });
56
- const clientCreds = getClientCredentials(creds);
57
- if (clientCreds) {
58
- const { session, apiKey } = await runtime.authenticate(clientCreds.clientId, clientCreds.clientSecret, target.oidcIssuer);
59
- await runtime.initPodData(session);
60
- const chatId = await runtime.getOrCreateDefaultChat(session);
61
- return { runtimeUrl: target.runtimeUrl, apiKey, session, chatId, runtime };
62
- }
63
- if (creds.authType === 'oidc_oauth') {
64
- const accessToken = await getOidcAccessToken(creds);
65
- if (!accessToken) {
66
- throw new Error('Failed to restore OIDC access token. Run `linx login` again.');
67
- }
68
- const pseudoSession = {
69
- async logout() { },
70
- };
71
- const podUrl = loadAccountSession()?.podUrl || creds.webId.replace('/card#me', '').replace(/\/?$/, '/');
72
- const session = {
73
- ...pseudoSession,
74
- info: {
75
- isLoggedIn: true,
76
- webId: creds.webId,
77
- podUrl,
78
- },
79
- fetch: (url, init) => runtime.authenticatedFetch(url, accessToken, init),
80
- };
81
- await runtime.initPodData(session);
82
- const chatId = await runtime.getOrCreateDefaultChat(session);
83
- return { runtimeUrl: target.runtimeUrl, apiKey: accessToken, session, chatId, runtime };
84
- }
85
- throw new Error('Unsupported LinX auth type. Run `linx login` again.');
78
+ const apiKey = await resolvePodRuntimeAuthToken(podSession);
79
+ const session = podSession.solidSession;
80
+ await runtime.initPodData(session);
81
+ const chatId = await runtime.getOrCreateDefaultChat(session);
82
+ return { runtimeUrl: target.runtimeUrl, apiKey, session, podSession, chatId, runtime };
86
83
  }
87
84
  async function resolveRuntimeAuthContext(urlOverride) {
88
85
  const runtime = await loadChatRuntime();
89
- const creds = loadCredentials();
90
- if (!creds) {
91
- throw new Error('No credentials found. Run `linx login` first.');
92
- }
86
+ const podSession = await createLinxPodDataSession();
93
87
  const target = resolveRuntimeTarget({
94
- issuerUrl: creds.url,
88
+ issuerUrl: podSession.credentials.url,
95
89
  runtimeUrlOverride: urlOverride,
96
90
  });
97
- const clientCreds = getClientCredentials(creds);
98
- if (clientCreds) {
99
- const { session, apiKey } = await runtime.authenticate(clientCreds.clientId, clientCreds.clientSecret, target.oidcIssuer);
100
- return { runtimeUrl: target.runtimeUrl, apiKey, session, runtime };
91
+ const apiKey = await resolvePodRuntimeAuthToken(podSession);
92
+ return {
93
+ runtimeUrl: target.runtimeUrl,
94
+ apiKey,
95
+ session: podSession.solidSession,
96
+ podSession,
97
+ runtime,
98
+ };
99
+ }
100
+ async function createLinxPodDataSession() {
101
+ if (!loadCredentials()) {
102
+ throw new Error('No credentials found. Run `linx login` first.');
103
+ }
104
+ const podSession = await createPodDataSession();
105
+ if (!podSession) {
106
+ throw new Error('Unsupported LinX auth type. Run `linx login` again.');
101
107
  }
102
- if (creds.authType === 'oidc_oauth') {
103
- const accessToken = await getOidcAccessToken(creds);
104
- if (!accessToken) {
105
- throw new Error('Failed to restore OIDC access token. Run `linx login` again.');
108
+ return podSession;
109
+ }
110
+ async function resolvePodRuntimeAuthToken(podSession) {
111
+ try {
112
+ return await podSession.getRuntimeAuthToken();
113
+ }
114
+ catch (error) {
115
+ if (isOidcLoginExpiredError(error)) {
116
+ throw new Error('LinX Cloud login expired. Run `linx login` to re-authorize.');
106
117
  }
107
- const pseudoSession = {
108
- async logout() { },
109
- };
110
- return { runtimeUrl: target.runtimeUrl, apiKey: accessToken, session: pseudoSession, runtime };
118
+ throw error;
111
119
  }
112
- throw new Error('Unsupported LinX auth type. Run `linx login` again.');
113
120
  }
114
121
  async function runSingleTurn(options) {
115
122
  const { ctx, threadId, model, prompt } = options;
@@ -142,7 +149,7 @@ async function runInteractive(options) {
142
149
  const { ctx, initialThreadId, initialModel, initialPrompt } = options;
143
150
  let threadId = initialThreadId;
144
151
  let model = initialModel;
145
- process.stdout.write(`LinX CLI ready\nthread: ${threadId}\nmodel: ${model || DEFAULT_LINX_CLOUD_MODEL_ID}\n输入 /help 查看命令。\n\n`);
152
+ process.stdout.write(`LinX CLI ready\nthread: ${threadId}\nmodel: ${model || DEFAULT_LINX_CLOUD_MODEL_ID}\n输入 /hotkeys 查看快捷键。\n\n`);
146
153
  if (initialPrompt) {
147
154
  await runSingleTurn({ ctx, threadId, model, prompt: initialPrompt });
148
155
  }
@@ -154,7 +161,7 @@ async function runInteractive(options) {
154
161
  break;
155
162
  }
156
163
  if (input === '/help') {
157
- process.stdout.write('/help 查看帮助\n/threads 列出 threads\n/new 新建 thread\n/use <threadId> 切换 thread\n/model <modelId> 切换模型\n/exit 退出\n\n');
164
+ process.stdout.write('/hotkeys 查看快捷键\n/threads 列出 threads\n/new 新建 thread\n/use <threadId> 切换 thread\n/model <modelId> 切换模型\n/exit 退出\n\n');
158
165
  continue;
159
166
  }
160
167
  if (input === '/threads') {
@@ -190,34 +197,73 @@ async function runInteractive(options) {
190
197
  await runSingleTurn({ ctx, threadId, model, prompt: input });
191
198
  }
192
199
  }
193
- async function runPiCommand(argv) {
194
- const backend = argv.backend ?? 'cloud';
195
- if (!argv.print && backend === 'cloud') {
196
- const { resolveLinxPiCloudOAuthCredential } = await import('./lib/pi-adapter/auth.js');
197
- const existingCredential = await resolveLinxPiCloudOAuthCredential(undefined);
198
- if (!existingCredential) {
199
- const answer = (await promptText('LinX Cloud not connected. Open browser login now? [Y/n] ')).trim().toLowerCase();
200
- const shouldLoginNow = answer === '' || answer === 'y' || answer === 'yes';
201
- if (shouldLoginNow) {
202
- const { ensureBrowserConsentLogin } = await import('./lib/oidc-auth.js');
203
- process.stdout.write('Opening LinX Cloud login in your browser...\n');
204
- try {
205
- const result = await ensureBrowserConsentLogin({
206
- issuerUrl: resolveAccountBaseUrl(),
207
- });
208
- if (result.reusedExistingSession) {
209
- process.stdout.write('Reused existing LinX Cloud session.\n');
210
- }
211
- }
212
- catch (error) {
213
- process.stdout.write('LinX Cloud login was not completed. Continuing into TUI without auth.\n');
214
- if (error instanceof Error && error.message.trim()) {
215
- process.stdout.write(`${error.message}\n`);
216
- }
217
- }
200
+ async function runLinxPackageCommand(command, options = {}) {
201
+ if ((command === 'install' || command === 'remove') && !options.source) {
202
+ throw new Error(`Missing ${command} source. Usage: linx ${command} <source> [-l]`);
203
+ }
204
+ const cwd = process.cwd();
205
+ const settingsManager = SettingsManager.create(cwd, LINX_AGENT_DIR);
206
+ const packageManager = new DefaultPackageManager({
207
+ cwd,
208
+ agentDir: LINX_AGENT_DIR,
209
+ settingsManager,
210
+ });
211
+ packageManager.setProgressCallback((event) => {
212
+ if (event.type === 'start' && event.message) {
213
+ process.stdout.write(`${event.message}\n`);
214
+ }
215
+ });
216
+ switch (command) {
217
+ case 'install':
218
+ await packageManager.installAndPersist(options.source, { local: Boolean(options.local) });
219
+ process.stdout.write(`Installed ${options.source}\n`);
220
+ return;
221
+ case 'remove': {
222
+ const removed = await packageManager.removeAndPersist(options.source, { local: Boolean(options.local) });
223
+ if (!removed) {
224
+ throw new Error(`No matching package found for ${options.source}`);
218
225
  }
226
+ process.stdout.write(`Removed ${options.source}\n`);
227
+ return;
219
228
  }
229
+ case 'update':
230
+ await packageManager.update(options.source);
231
+ process.stdout.write(options.source ? `Updated ${options.source}\n` : 'Updated packages\n');
232
+ return;
233
+ case 'list':
234
+ printConfiguredLinxPackages(packageManager);
235
+ return;
236
+ }
237
+ }
238
+ function printConfiguredLinxPackages(packageManager) {
239
+ const configuredPackages = packageManager.listConfiguredPackages();
240
+ if (configuredPackages.length === 0) {
241
+ process.stdout.write('No packages installed.\n');
242
+ return;
220
243
  }
244
+ const printGroup = (title, packages) => {
245
+ if (packages.length === 0) {
246
+ return;
247
+ }
248
+ process.stdout.write(`${title}:\n`);
249
+ for (const pkg of packages) {
250
+ const display = pkg.filtered ? `${pkg.source} (filtered)` : pkg.source;
251
+ process.stdout.write(` ${display}\n`);
252
+ if (pkg.installedPath) {
253
+ process.stdout.write(` ${pkg.installedPath}\n`);
254
+ }
255
+ }
256
+ };
257
+ printGroup('User packages', configuredPackages.filter((pkg) => pkg.scope === 'user'));
258
+ printGroup('Project packages', configuredPackages.filter((pkg) => pkg.scope === 'project'));
259
+ }
260
+ async function runPiCommand(argv) {
261
+ const backend = argv.backend ?? 'cloud';
262
+ const startupLoginPrompt = await resolveLinxStartupLoginPromptDecision({
263
+ backend,
264
+ print: argv.print,
265
+ issuerUrl: resolveAccountBaseUrl(),
266
+ });
221
267
  const adapter = createPiRuntimeAdapter({
222
268
  createNativeProxy(options) {
223
269
  return createCodexNativeProxy({
@@ -232,11 +278,11 @@ async function runPiCommand(argv) {
232
278
  },
233
279
  async listRemoteModels(session, runtimeUrl, apiKey) {
234
280
  const chatApi = await import('./lib/chat-api.js');
235
- return chatApi.listRemoteModels(session, runtimeUrl, apiKey);
281
+ return chatApi.listRemoteModels(session, runtimeUrl, apiKey, { fallback: false, timeoutMs: 5000 });
236
282
  },
237
283
  }, {
238
284
  cwd: argv.cwd || process.cwd(),
239
- model: argv.model || DEFAULT_LINX_CLOUD_MODEL_ID,
285
+ model: argv.model,
240
286
  backend,
241
287
  port: argv.port,
242
288
  providerConfig: {
@@ -245,13 +291,39 @@ async function runPiCommand(argv) {
245
291
  },
246
292
  });
247
293
  await adapter.start();
248
- const { SessionManager } = await import('@mariozechner/pi-coding-agent');
294
+ const sessionManager = await createLinxPiSessionManager({
295
+ cwd: adapter.cwd,
296
+ agentDir: LINX_AGENT_DIR,
297
+ session: argv.session,
298
+ last: argv.last,
299
+ });
249
300
  const runtime = await adapter.createRuntime({
250
301
  cwd: adapter.cwd,
251
302
  agentDir: LINX_AGENT_DIR,
252
- sessionManager: SessionManager.inMemory(adapter.cwd),
303
+ sessionManager,
304
+ });
305
+ const podMirror = new LinxPiPodMirror({
306
+ cwd: adapter.cwd,
307
+ sessionManager,
308
+ onError(error) {
309
+ if (process.env.LINX_DEBUG === '1') {
310
+ const message = error instanceof Error ? error.stack || error.message : String(error);
311
+ process.stderr.write(`[linx pod mirror] ${message}\n`);
312
+ }
313
+ },
314
+ });
315
+ const unsubscribePodMirror = runtime.session.subscribe((event) => {
316
+ podMirror.handleEvent(event);
253
317
  });
254
318
  const interactive = bootstrapPiInteractiveMode(runtime);
319
+ const bridge = runtime;
320
+ const loginPromptReason = resolveLinxInteractiveLoginReason({
321
+ startupDecision: startupLoginPrompt,
322
+ runtimePromptOnStart: bridge.linxAuthBridge?.shouldPromptLoginOnStart,
323
+ });
324
+ if (loginPromptReason) {
325
+ interactive.requestLogin?.(loginPromptReason);
326
+ }
255
327
  try {
256
328
  if (argv.print) {
257
329
  const prompt = (argv.prompt ?? []).join(' ').trim();
@@ -267,6 +339,8 @@ async function runPiCommand(argv) {
267
339
  await interactive.run();
268
340
  }
269
341
  finally {
342
+ unsubscribePodMirror();
343
+ await podMirror.close().catch(() => undefined);
270
344
  interactive.stop();
271
345
  await adapter.close();
272
346
  }
@@ -279,7 +353,7 @@ function buildPiCommand(command) {
279
353
  })
280
354
  .option('model', {
281
355
  type: 'string',
282
- describe: 'Model id to expose through the Pi runtime adapter',
356
+ describe: 'Model id to expose through the Pi runtime adapter; defaults to the last LinX selection',
283
357
  })
284
358
  .option('backend', {
285
359
  type: 'string',
@@ -301,6 +375,15 @@ function buildPiCommand(command) {
301
375
  type: 'boolean',
302
376
  default: false,
303
377
  describe: 'Run a single prompt without entering interactive mode',
378
+ })
379
+ .option('session', {
380
+ type: 'string',
381
+ describe: 'Resume a specific LinX/Pi session id or JSONL file',
382
+ })
383
+ .option('last', {
384
+ type: 'boolean',
385
+ default: false,
386
+ describe: 'Continue the most recent local LinX/Pi session for this workspace',
304
387
  })
305
388
  .positional('prompt', {
306
389
  array: true,
@@ -338,7 +421,7 @@ const execCommand = {
338
421
  };
339
422
  const cli = yargs(hideBin(process.argv))
340
423
  .scriptName('linx')
341
- .version(LINX_CLI_VERSION)
424
+ .version(readPackageVersion())
342
425
  .parserConfiguration({
343
426
  'populate--': true,
344
427
  })
@@ -346,6 +429,30 @@ const cli = yargs(hideBin(process.argv))
346
429
  .command(logoutCommand)
347
430
  .command(whoamiCommand)
348
431
  .command(aiCommand)
432
+ .command('install [source]', 'Install a LinX package or extension', (command) => command
433
+ .positional('source', { type: 'string', describe: 'Package source to install' })
434
+ .option('local', { alias: 'l', type: 'boolean', default: false, describe: 'Install project-locally (.pi/settings.json)' }), async (argv) => {
435
+ await runLinxPackageCommand('install', {
436
+ source: typeof argv.source === 'string' ? argv.source : undefined,
437
+ local: Boolean(argv.local),
438
+ });
439
+ })
440
+ .command('remove [source]', 'Remove a LinX package or extension', (command) => command
441
+ .positional('source', { type: 'string', describe: 'Package source to remove' })
442
+ .option('local', { alias: 'l', type: 'boolean', default: false, describe: 'Remove from project settings (.pi/settings.json)' }), async (argv) => {
443
+ await runLinxPackageCommand('remove', {
444
+ source: typeof argv.source === 'string' ? argv.source : undefined,
445
+ local: Boolean(argv.local),
446
+ });
447
+ })
448
+ .command('update [source]', 'Update installed LinX packages', (command) => command.positional('source', { type: 'string', describe: 'Package source to update' }), async (argv) => {
449
+ await runLinxPackageCommand('update', {
450
+ source: typeof argv.source === 'string' ? argv.source : undefined,
451
+ });
452
+ })
453
+ .command('list', 'List installed LinX packages', () => undefined, async () => {
454
+ await runLinxPackageCommand('list');
455
+ })
349
456
  .command(execCommand)
350
457
  .command(defaultPiCommand)
351
458
  .command('chat [prompt..]', false, (command) => command
@@ -364,11 +471,11 @@ const cli = yargs(hideBin(process.argv))
364
471
  const prompt = argv.prompt?.join(' ').trim() || undefined;
365
472
  if (prompt) {
366
473
  await runSingleTurn({ ctx, threadId, model: argv.model, prompt });
367
- await ctx.session.logout();
474
+ await ctx.podSession.close();
368
475
  return;
369
476
  }
370
477
  await runInteractive({ ctx, initialThreadId: threadId, initialModel: argv.model });
371
- await ctx.session.logout();
478
+ await ctx.podSession.close();
372
479
  })
373
480
  .command('models', 'List available remote models', (command) => command.option('url', { type: 'string', describe: 'Runtime API base URL override' }), async (argv) => {
374
481
  const ctx = await resolveRuntimeAuthContext(argv.url);
@@ -385,24 +492,44 @@ const cli = yargs(hideBin(process.argv))
385
492
  }
386
493
  else {
387
494
  for (const model of models) {
388
- const meta = [model.provider || model.ownedBy, model.contextWindow ? `${model.contextWindow}` : '']
389
- .filter(Boolean)
390
- .join(' · ');
495
+ const meta = formatRemoteModelMetadata(model);
391
496
  process.stdout.write(`- ${model.id}${meta ? ` (${meta})` : ''}\n`);
392
497
  }
393
498
  }
394
- await ctx.session.logout();
499
+ await ctx.podSession.close();
395
500
  })
396
- .command('resume', 'List resumable CLI threads', (command) => command.option('url', { type: 'string', describe: 'Runtime API base URL override' }), async (argv) => {
397
- const ctx = await resolveContext(argv.url);
398
- const threads = await ctx.runtime.listThreads(ctx.session, ctx.chatId);
399
- if (threads.length === 0) {
400
- process.stdout.write('No threads found.\n');
401
- }
402
- else {
403
- process.stdout.write(`${threads.map((thread) => `- ${ctx.runtime.formatThreadLabel(thread)}`).join('\n')}\n`);
501
+ .command('resume [session]', 'Resume a previous interactive LinX session', (command) => command
502
+ .positional('session', { type: 'string', describe: 'Session id/prefix or JSONL file to resume' })
503
+ .option('last', { type: 'boolean', default: false, describe: 'Resume the most recent local session for this workspace' })
504
+ .option('cwd', { type: 'string', describe: 'Workspace path for the resumed session' })
505
+ .option('model', { type: 'string', describe: 'Model id to expose through the Pi runtime adapter' })
506
+ .option('backend', {
507
+ type: 'string',
508
+ default: 'cloud',
509
+ choices: ['cloud', 'native'],
510
+ describe: 'Backend mode. Default is cloud; native keeps the local Codex proxy for debugging only.',
511
+ })
512
+ .option('port', { type: 'number', default: 8787, describe: 'Local websocket port used only when --backend native' })
513
+ .option('runtime-url', { type: 'string', default: 'https://api.undefineds.co/v1', describe: 'Cloud runtime API base URL' }), async (argv) => {
514
+ const session = typeof argv.session === 'string' ? argv.session : undefined;
515
+ if (!session && !argv.last) {
516
+ const sessions = await listLinxPiSessions(argv.cwd || process.cwd(), LINX_AGENT_DIR);
517
+ if (sessions.length === 0) {
518
+ process.stdout.write('No LinX sessions found.\n');
519
+ return;
520
+ }
521
+ process.stdout.write(`${sessions.map(formatLinxPiSessionSummary).join('\n')}\n`);
522
+ return;
404
523
  }
405
- await ctx.session.logout();
524
+ await runPiCommand({
525
+ cwd: argv.cwd,
526
+ model: argv.model,
527
+ backend: argv.backend,
528
+ port: argv.port,
529
+ 'runtime-url': argv['runtime-url'],
530
+ session,
531
+ last: argv.last || !session,
532
+ });
406
533
  })
407
534
  .command('fork [thread]', 'Fork a previous interactive session', (command) => command
408
535
  .positional('thread', { type: 'string', describe: 'Thread ID to fork' })
@@ -448,11 +575,11 @@ const cli = yargs(hideBin(process.argv))
448
575
  .command('watch <action> [backend] [prompt..]', 'Run or inspect local watch backends', (command) => command
449
576
  .positional('action', {
450
577
  type: 'string',
451
- choices: ['run', 'backends', 'sessions', 'show', 'approvals', 'approve', 'reject', 'codex', 'claude', 'codebuddy'],
578
+ choices: ['run', 'backends', 'sessions', 'show', 'codex', 'claude', 'codebuddy'],
452
579
  })
453
580
  .positional('backend', {
454
581
  type: 'string',
455
- describe: 'Watch backend for `run`, session id for `show`, or approval id for `approve|reject`',
582
+ describe: 'Watch backend for `run` or session id for `show`',
456
583
  })
457
584
  .option('mode', {
458
585
  type: 'string',
@@ -479,14 +606,11 @@ const cli = yargs(hideBin(process.argv))
479
606
  choices: ['auto', 'local', 'cloud'],
480
607
  describe: 'Resolve credentials only: local CLI login, LinX cloud config, or auto fallback. Runtime still runs locally.',
481
608
  })
482
- .option('session', {
483
- type: 'boolean',
484
- default: false,
485
- describe: 'Approve for the current watch session instead of only once.',
486
- })
487
- .option('reason', {
609
+ .option('approval-source', {
488
610
  type: 'string',
489
- describe: 'Optional note recorded with an approval decision.',
611
+ default: 'hybrid',
612
+ choices: ['local', 'remote', 'hybrid'],
613
+ describe: 'Resolve backend approval requests locally, through Pod remote approvals, or whichever answers first.',
490
614
  }), async (argv) => {
491
615
  const rawAction = String(argv.action);
492
616
  const directBackend = ['codex', 'claude', 'codebuddy'].includes(rawAction);
@@ -523,30 +647,6 @@ const cli = yargs(hideBin(process.argv))
523
647
  process.stdout.write(formatArchivedWatchSession(session, loadArchivedWatchEvents(sessionId)));
524
648
  return;
525
649
  }
526
- if (action === 'approvals') {
527
- const approvals = await listRemoteWatchApprovals();
528
- if (approvals.length === 0) {
529
- process.stdout.write('No pending remote approvals in the approval inbox.\n');
530
- return;
531
- }
532
- process.stdout.write(`${approvals.map(formatRemoteWatchApprovalSummary).join('\n')}\n`);
533
- return;
534
- }
535
- if (action === 'approve' || action === 'reject') {
536
- const approvalId = argv.backend ? String(argv.backend) : '';
537
- if (!approvalId) {
538
- throw new Error(`Usage: linx watch ${action} <approvalId>`);
539
- }
540
- const summary = await resolveRemoteWatchApproval({
541
- approvalId,
542
- decision: action === 'approve'
543
- ? (argv.session ? 'accept_for_session' : 'accept')
544
- : 'decline',
545
- note: argv.reason ? String(argv.reason) : undefined,
546
- });
547
- process.stdout.write(`${formatRemoteWatchApprovalSummary(summary)}\n`);
548
- return;
549
- }
550
650
  const backend = (directBackend ? rawAction : argv.backend);
551
651
  if (!backend || !['codex', 'claude', 'codebuddy'].includes(backend)) {
552
652
  throw new Error('Usage: linx watch run <codex|claude|codebuddy> <prompt> [-- backend args]\n or: linx watch <codex|claude|codebuddy> <prompt>');
@@ -565,13 +665,26 @@ const cli = yargs(hideBin(process.argv))
565
665
  prompt,
566
666
  passthroughArgs: (argv['--'] ?? []).map(String),
567
667
  credentialSource: argv['credential-source'],
668
+ approvalSource: argv['approval-source'],
568
669
  });
569
670
  if (exitCode !== 0) {
570
671
  process.exitCode = exitCode;
571
672
  }
572
673
  })
573
674
  .strict()
574
- .help();
675
+ .help()
676
+ .fail((message, error, yargsInstance) => {
677
+ if (error) {
678
+ console.error(error instanceof Error ? error.message : String(error));
679
+ process.exit(1);
680
+ }
681
+ if (message) {
682
+ console.error(message);
683
+ process.exit(1);
684
+ }
685
+ yargsInstance.showHelp();
686
+ process.exit(1);
687
+ });
575
688
  process.on('unhandledRejection', (error) => {
576
689
  console.error(error instanceof Error ? error.message : String(error));
577
690
  process.exit(1);