@undefineds.co/linx 0.2.1
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/README.md +91 -0
- package/dist/index.js +578 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/account-api.js +100 -0
- package/dist/lib/account-api.js.map +1 -0
- package/dist/lib/account-session.js +30 -0
- package/dist/lib/account-session.js.map +1 -0
- package/dist/lib/ai-command.js +411 -0
- package/dist/lib/ai-command.js.map +1 -0
- package/dist/lib/chat-api.js +122 -0
- package/dist/lib/chat-api.js.map +1 -0
- package/dist/lib/codex-plugin/bridge.js +109 -0
- package/dist/lib/codex-plugin/bridge.js.map +1 -0
- package/dist/lib/codex-plugin/codex-native-proxy.js +358 -0
- package/dist/lib/codex-plugin/codex-native-proxy.js.map +1 -0
- package/dist/lib/codex-plugin/index.js +4 -0
- package/dist/lib/codex-plugin/index.js.map +1 -0
- package/dist/lib/codex-plugin/runner.js +38 -0
- package/dist/lib/codex-plugin/runner.js.map +1 -0
- package/dist/lib/credentials-store.js +74 -0
- package/dist/lib/credentials-store.js.map +1 -0
- package/dist/lib/default-model.js +8 -0
- package/dist/lib/default-model.js.map +1 -0
- package/dist/lib/login-command.js +51 -0
- package/dist/lib/login-command.js.map +1 -0
- package/dist/lib/models.js +6 -0
- package/dist/lib/models.js.map +1 -0
- package/dist/lib/oidc-auth.js +451 -0
- package/dist/lib/oidc-auth.js.map +1 -0
- package/dist/lib/oidc-session-storage.js +37 -0
- package/dist/lib/oidc-session-storage.js.map +1 -0
- package/dist/lib/pi-adapter/auth.js +38 -0
- package/dist/lib/pi-adapter/auth.js.map +1 -0
- package/dist/lib/pi-adapter/branding.js +239 -0
- package/dist/lib/pi-adapter/branding.js.map +1 -0
- package/dist/lib/pi-adapter/index.js +4 -0
- package/dist/lib/pi-adapter/index.js.map +1 -0
- package/dist/lib/pi-adapter/interactive.js +63 -0
- package/dist/lib/pi-adapter/interactive.js.map +1 -0
- package/dist/lib/pi-adapter/runtime.js +271 -0
- package/dist/lib/pi-adapter/runtime.js.map +1 -0
- package/dist/lib/pi-adapter/stream.js +314 -0
- package/dist/lib/pi-adapter/stream.js.map +1 -0
- package/dist/lib/pi-adapter/theme.js +84 -0
- package/dist/lib/pi-adapter/theme.js.map +1 -0
- package/dist/lib/pod-chat-store.js +200 -0
- package/dist/lib/pod-chat-store.js.map +1 -0
- package/dist/lib/profile-identity.js +116 -0
- package/dist/lib/profile-identity.js.map +1 -0
- package/dist/lib/prompt.js +82 -0
- package/dist/lib/prompt.js.map +1 -0
- package/dist/lib/runtime-target.js +12 -0
- package/dist/lib/runtime-target.js.map +1 -0
- package/dist/lib/solid-auth.js +55 -0
- package/dist/lib/solid-auth.js.map +1 -0
- package/dist/lib/thread-utils.js +12 -0
- package/dist/lib/thread-utils.js.map +1 -0
- package/dist/lib/watch/archive.js +110 -0
- package/dist/lib/watch/archive.js.map +1 -0
- package/dist/lib/watch/auth.js +56 -0
- package/dist/lib/watch/auth.js.map +1 -0
- package/dist/lib/watch/codex-composer.js +219 -0
- package/dist/lib/watch/codex-composer.js.map +1 -0
- package/dist/lib/watch/codex-footer.js +88 -0
- package/dist/lib/watch/codex-footer.js.map +1 -0
- package/dist/lib/watch/codex-overlay.js +154 -0
- package/dist/lib/watch/codex-overlay.js.map +1 -0
- package/dist/lib/watch/codex-request-form.js +341 -0
- package/dist/lib/watch/codex-request-form.js.map +1 -0
- package/dist/lib/watch/codex-request-input.js +187 -0
- package/dist/lib/watch/codex-request-input.js.map +1 -0
- package/dist/lib/watch/display.js +1514 -0
- package/dist/lib/watch/display.js.map +1 -0
- package/dist/lib/watch/format.js +161 -0
- package/dist/lib/watch/format.js.map +1 -0
- package/dist/lib/watch/hooks/claude.js +12 -0
- package/dist/lib/watch/hooks/claude.js.map +1 -0
- package/dist/lib/watch/hooks/codebuddy.js +18 -0
- package/dist/lib/watch/hooks/codebuddy.js.map +1 -0
- package/dist/lib/watch/hooks/codex.js +13 -0
- package/dist/lib/watch/hooks/codex.js.map +1 -0
- package/dist/lib/watch/hooks/index.js +24 -0
- package/dist/lib/watch/hooks/index.js.map +1 -0
- package/dist/lib/watch/hooks/shared.js +19 -0
- package/dist/lib/watch/hooks/shared.js.map +1 -0
- package/dist/lib/watch/index.js +10 -0
- package/dist/lib/watch/index.js.map +1 -0
- package/dist/lib/watch/pod-ai.js +168 -0
- package/dist/lib/watch/pod-ai.js.map +1 -0
- package/dist/lib/watch/pod-approval.js +578 -0
- package/dist/lib/watch/pod-approval.js.map +1 -0
- package/dist/lib/watch/pod-persistence.js +226 -0
- package/dist/lib/watch/pod-persistence.js.map +1 -0
- package/dist/lib/watch/runner.js +1124 -0
- package/dist/lib/watch/runner.js.map +1 -0
- package/dist/lib/watch/types.js +2 -0
- package/dist/lib/watch/types.js.map +1 -0
- package/dist/watch-cli.js +142 -0
- package/dist/watch-cli.js.map +1 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# LinX CLI
|
|
2
|
+
|
|
3
|
+
最小用户聊天 CLI,复用 LinX 的 Pod 数据模型和 LinX server 的 OpenAI-compatible API。
|
|
4
|
+
|
|
5
|
+
- cloud account/login 默认走 `https://id.undefineds.co/.account/*`
|
|
6
|
+
- cloud chat/models 走 live `https://api.undefineds.co/v1/*`
|
|
7
|
+
- 内置 discovery snapshot 只做离线 fallback,不替代 live `/v1/models`
|
|
8
|
+
- 官方云默认分流:`id` 负责 Solid/OIDC,`pods` 负责 Pod 托管域,`api` 负责 chat/models runtime;自建 Pod 默认仍走同源
|
|
9
|
+
|
|
10
|
+
## Commands
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# 浏览器授权登录并保存本地 OIDC 会话
|
|
14
|
+
# 默认用官方 cloud identity:https://id.undefineds.co
|
|
15
|
+
yarn workspace @undefineds.co/linx dev login
|
|
16
|
+
|
|
17
|
+
# 自建 / 本地 issuer 时再显式覆盖
|
|
18
|
+
yarn workspace @undefineds.co/linx dev login --url http://localhost:3000
|
|
19
|
+
|
|
20
|
+
# 查看 / 清理当前本地登录态
|
|
21
|
+
yarn workspace @undefineds.co/linx dev whoami --verbose
|
|
22
|
+
yarn workspace @undefineds.co/linx dev logout
|
|
23
|
+
|
|
24
|
+
# 把云端 AI provider 凭据写进 Pod
|
|
25
|
+
yarn workspace @undefineds.co/linx dev ai connect claude --api-key sk-ant-xxx --model claude-sonnet-4-20250514
|
|
26
|
+
yarn workspace @undefineds.co/linx dev ai status claude
|
|
27
|
+
yarn workspace @undefineds.co/linx dev ai disconnect claude
|
|
28
|
+
|
|
29
|
+
# 列出远程可用模型
|
|
30
|
+
yarn workspace @undefineds.co/linx dev models
|
|
31
|
+
|
|
32
|
+
# 单轮聊天
|
|
33
|
+
yarn workspace @undefineds.co/linx dev chat "帮我总结一下今天的工作"
|
|
34
|
+
|
|
35
|
+
# 进入默认 Pi TUI
|
|
36
|
+
yarn workspace @undefineds.co/linx dev
|
|
37
|
+
|
|
38
|
+
# 继续最近一次 thread
|
|
39
|
+
yarn workspace @undefineds.co/linx dev chat --continue
|
|
40
|
+
|
|
41
|
+
# 本地 watch(多轮 REPL + 结构化留档)
|
|
42
|
+
yarn workspace @undefineds.co/linx dev watch run codex
|
|
43
|
+
yarn workspace @undefineds.co/linx dev watch run claude "先总结这个目录的职责"
|
|
44
|
+
yarn workspace @undefineds.co/linx dev watch run codebuddy -- --tools Read,Edit
|
|
45
|
+
yarn workspace @undefineds.co/linx dev watch backends
|
|
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"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Slash Commands
|
|
53
|
+
|
|
54
|
+
- `/help` 查看帮助
|
|
55
|
+
- `/threads` 查看最近 threads
|
|
56
|
+
- `/new` 新建 thread
|
|
57
|
+
- `/use <threadId>` 切换 thread
|
|
58
|
+
- `/model <modelId>` 切换模型
|
|
59
|
+
- `/exit` 退出
|
|
60
|
+
|
|
61
|
+
## Credentials
|
|
62
|
+
|
|
63
|
+
当前优先读取:
|
|
64
|
+
|
|
65
|
+
1. `~/.linx/config.json` + `~/.linx/secrets.json`
|
|
66
|
+
|
|
67
|
+
## Local Watch Notes
|
|
68
|
+
|
|
69
|
+
- `watch run` 当前直接依赖本机已经安装好的 `codex` / `claude` / `codebuddy`
|
|
70
|
+
- 如果当前终端对全屏重绘支持不好,可加 `--plain`(等价于 `LINX_WATCH_PLAIN=1`)关闭全屏 TUI,改用线性输出
|
|
71
|
+
- LinX 负责统一 `manual | smart | auto` 模式,并把会话元数据写到 `~/.linx/watch/sessions/`
|
|
72
|
+
- `--credential-source local|cloud|auto` 只决定凭据来源;`watch` 当前运行时始终是本地,不会因为选 cloud credential source 就切成 cloud runtime
|
|
73
|
+
- `--credential-source cloud` 当前可显式用于 `codex` / `claude` / `codebuddy`,前提是对应 API key 已写进 Pod
|
|
74
|
+
- 单本地会话时,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 的主交互路径
|
|
78
|
+
- 当前是最小多轮版:本地 REPL、统一 ACP 会话、归档结构化事件
|
|
79
|
+
- 在交互式 TTY 里,`watch run` 会默认进入全屏 TUI;非 TTY / 管道输出会自动降级到 plain mode
|
|
80
|
+
- `linx watch show <sessionId>` 现在会回放归档 timeline,而不是直接输出 `session.json`
|
|
81
|
+
- `codex` 走 `codex-acp`,`claude` 走 `claude-code-acp`,`codebuddy` 走内置 `--acp --acp-transport stdio`
|
|
82
|
+
- 当前 `linx watch run codex` 的前台仍是 LinX watch TUI,不是 Codex 原生 TUI;真正执行任务与工具调用的是 `codex-acp`
|
|
83
|
+
- Codex 原生壳相关集成不放在 LinX watch 壳里维护;后台桥接能力位于 `apps/cli/src/lib/codex-plugin/*`,按 plugin/sidecar 语义组织
|
|
84
|
+
- LinX 不再维护各家 native / 非 ACP JSON 输出兼容层,统一按 ACP 处理多轮会话、权限请求和结构化输入
|
|
85
|
+
- 仓库内 `yarn workspace @undefineds.co/linx dev watch ...` 不再依赖 `tsx`,会直接编译并运行独立 watch 入口
|
|
86
|
+
- `--` 后面的参数会原样透传给对应后端 CLI
|
|
87
|
+
- 当前只支持 `local runtime + remote approval`;不支持本地 runtime 退出后由云端接管执行
|
|
88
|
+
|
|
89
|
+
## TODO
|
|
90
|
+
|
|
91
|
+
- blocked by xpod: `watch --runtime cloud`
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,578 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import yargs from 'yargs';
|
|
3
|
+
import { hideBin } from 'yargs/helpers';
|
|
4
|
+
import { aiCommand } from './lib/ai-command.js';
|
|
5
|
+
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 { loginCommand, logoutCommand, whoamiCommand } from './lib/login-command.js';
|
|
9
|
+
import { runPrintMode } from '@mariozechner/pi-coding-agent';
|
|
10
|
+
import { promptText } from './lib/prompt.js';
|
|
11
|
+
import { resolveRuntimeTarget } from './lib/runtime-target.js';
|
|
12
|
+
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';
|
|
16
|
+
import { LINX_AGENT_DIR } from './lib/pi-adapter/branding.js';
|
|
17
|
+
import { formatRemoteWatchApprovalSummary, formatArchivedWatchSession, formatWatchSessionSummary, loadArchivedWatchEvents, listArchivedWatchSessions, listRemoteWatchApprovals, listSupportedWatchBackends, loadArchivedWatchSession, resolveRemoteWatchApproval, runWatch, } from './lib/watch/index.js';
|
|
18
|
+
let chatRuntimePromise = null;
|
|
19
|
+
async function loadChatRuntime() {
|
|
20
|
+
if (!chatRuntimePromise) {
|
|
21
|
+
chatRuntimePromise = Promise.all([
|
|
22
|
+
import('./lib/chat-api.js'),
|
|
23
|
+
import('./lib/pod-chat-store.js'),
|
|
24
|
+
import('./lib/solid-auth.js'),
|
|
25
|
+
]).then(([chatApi, podChatStore, solidAuth]) => ({
|
|
26
|
+
createRemoteCompletion: chatApi.createRemoteCompletion,
|
|
27
|
+
listRemoteModels: chatApi.listRemoteModels,
|
|
28
|
+
createThread: podChatStore.createThread,
|
|
29
|
+
formatThreadLabel: podChatStore.formatThreadLabel,
|
|
30
|
+
getLatestThreadId: podChatStore.getLatestThreadId,
|
|
31
|
+
getOrCreateDefaultChat: podChatStore.getOrCreateDefaultChat,
|
|
32
|
+
initPodData: podChatStore.initPodData,
|
|
33
|
+
listThreads: podChatStore.listThreads,
|
|
34
|
+
loadMessages: podChatStore.loadMessages,
|
|
35
|
+
loadThread: podChatStore.loadThread,
|
|
36
|
+
saveAssistantMessage: podChatStore.saveAssistantMessage,
|
|
37
|
+
saveUserMessage: podChatStore.saveUserMessage,
|
|
38
|
+
toOpenAiMessages: podChatStore.toOpenAiMessages,
|
|
39
|
+
authenticate: solidAuth.authenticate,
|
|
40
|
+
authenticatedFetch: solidAuth.authenticatedFetch,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
return chatRuntimePromise;
|
|
44
|
+
}
|
|
45
|
+
async function resolveContext(urlOverride) {
|
|
46
|
+
const runtime = await loadChatRuntime();
|
|
47
|
+
const creds = loadCredentials();
|
|
48
|
+
if (!creds) {
|
|
49
|
+
throw new Error('No credentials found. Run `linx login` first.');
|
|
50
|
+
}
|
|
51
|
+
const target = resolveRuntimeTarget({
|
|
52
|
+
issuerUrl: creds.url,
|
|
53
|
+
runtimeUrlOverride: urlOverride,
|
|
54
|
+
});
|
|
55
|
+
const clientCreds = getClientCredentials(creds);
|
|
56
|
+
if (clientCreds) {
|
|
57
|
+
const { session, apiKey } = await runtime.authenticate(clientCreds.clientId, clientCreds.clientSecret, target.oidcIssuer);
|
|
58
|
+
await runtime.initPodData(session);
|
|
59
|
+
const chatId = await runtime.getOrCreateDefaultChat(session);
|
|
60
|
+
return { runtimeUrl: target.runtimeUrl, apiKey, session, chatId, runtime };
|
|
61
|
+
}
|
|
62
|
+
if (creds.authType === 'oidc_oauth') {
|
|
63
|
+
const accessToken = await getOidcAccessToken(creds);
|
|
64
|
+
if (!accessToken) {
|
|
65
|
+
throw new Error('Failed to restore OIDC access token. Run `linx login` again.');
|
|
66
|
+
}
|
|
67
|
+
const pseudoSession = {
|
|
68
|
+
async logout() { },
|
|
69
|
+
};
|
|
70
|
+
const podUrl = loadAccountSession()?.podUrl || creds.webId.replace('/card#me', '').replace(/\/?$/, '/');
|
|
71
|
+
const session = {
|
|
72
|
+
...pseudoSession,
|
|
73
|
+
info: {
|
|
74
|
+
isLoggedIn: true,
|
|
75
|
+
webId: creds.webId,
|
|
76
|
+
podUrl,
|
|
77
|
+
},
|
|
78
|
+
fetch: (url, init) => runtime.authenticatedFetch(url, accessToken, init),
|
|
79
|
+
};
|
|
80
|
+
await runtime.initPodData(session);
|
|
81
|
+
const chatId = await runtime.getOrCreateDefaultChat(session);
|
|
82
|
+
return { runtimeUrl: target.runtimeUrl, apiKey: accessToken, session, chatId, runtime };
|
|
83
|
+
}
|
|
84
|
+
throw new Error('Unsupported LinX auth type. Run `linx login` again.');
|
|
85
|
+
}
|
|
86
|
+
async function resolveRuntimeAuthContext(urlOverride) {
|
|
87
|
+
const runtime = await loadChatRuntime();
|
|
88
|
+
const creds = loadCredentials();
|
|
89
|
+
if (!creds) {
|
|
90
|
+
throw new Error('No credentials found. Run `linx login` first.');
|
|
91
|
+
}
|
|
92
|
+
const target = resolveRuntimeTarget({
|
|
93
|
+
issuerUrl: creds.url,
|
|
94
|
+
runtimeUrlOverride: urlOverride,
|
|
95
|
+
});
|
|
96
|
+
const clientCreds = getClientCredentials(creds);
|
|
97
|
+
if (clientCreds) {
|
|
98
|
+
const { session, apiKey } = await runtime.authenticate(clientCreds.clientId, clientCreds.clientSecret, target.oidcIssuer);
|
|
99
|
+
return { runtimeUrl: target.runtimeUrl, apiKey, session, runtime };
|
|
100
|
+
}
|
|
101
|
+
if (creds.authType === 'oidc_oauth') {
|
|
102
|
+
const accessToken = await getOidcAccessToken(creds);
|
|
103
|
+
if (!accessToken) {
|
|
104
|
+
throw new Error('Failed to restore OIDC access token. Run `linx login` again.');
|
|
105
|
+
}
|
|
106
|
+
const pseudoSession = {
|
|
107
|
+
async logout() { },
|
|
108
|
+
};
|
|
109
|
+
return { runtimeUrl: target.runtimeUrl, apiKey: accessToken, session: pseudoSession, runtime };
|
|
110
|
+
}
|
|
111
|
+
throw new Error('Unsupported LinX auth type. Run `linx login` again.');
|
|
112
|
+
}
|
|
113
|
+
async function runSingleTurn(options) {
|
|
114
|
+
const { ctx, threadId, model, prompt } = options;
|
|
115
|
+
const history = await ctx.runtime.loadMessages(ctx.session, threadId);
|
|
116
|
+
await ctx.runtime.saveUserMessage(ctx.session, ctx.chatId, threadId, prompt);
|
|
117
|
+
const reply = await ctx.runtime.createRemoteCompletion({
|
|
118
|
+
runtimeUrl: ctx.runtimeUrl,
|
|
119
|
+
apiKey: ctx.apiKey,
|
|
120
|
+
model,
|
|
121
|
+
messages: [...ctx.runtime.toOpenAiMessages(history), { role: 'user', content: prompt }],
|
|
122
|
+
});
|
|
123
|
+
const replyText = typeof reply === 'string' ? reply : reply.content ?? '';
|
|
124
|
+
await ctx.runtime.saveAssistantMessage(ctx.session, ctx.chatId, threadId, replyText);
|
|
125
|
+
process.stdout.write(`\n${replyText}\n\n`);
|
|
126
|
+
}
|
|
127
|
+
async function resolveThreadId(options) {
|
|
128
|
+
const { ctx, continueMode, explicitThreadId, workspace } = options;
|
|
129
|
+
if (explicitThreadId) {
|
|
130
|
+
return explicitThreadId;
|
|
131
|
+
}
|
|
132
|
+
if (continueMode) {
|
|
133
|
+
const latest = await ctx.runtime.getLatestThreadId(ctx.session, ctx.chatId);
|
|
134
|
+
if (latest) {
|
|
135
|
+
return latest;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return ctx.runtime.createThread(ctx.session, ctx.chatId, workspace || process.cwd(), 'CLI Session');
|
|
139
|
+
}
|
|
140
|
+
async function runInteractive(options) {
|
|
141
|
+
const { ctx, initialThreadId, initialModel, initialPrompt } = options;
|
|
142
|
+
let threadId = initialThreadId;
|
|
143
|
+
let model = initialModel;
|
|
144
|
+
process.stdout.write(`LinX CLI ready\nthread: ${threadId}\nmodel: ${model || DEFAULT_LINX_CLOUD_MODEL_ID}\n输入 /help 查看命令。\n\n`);
|
|
145
|
+
if (initialPrompt) {
|
|
146
|
+
await runSingleTurn({ ctx, threadId, model, prompt: initialPrompt });
|
|
147
|
+
}
|
|
148
|
+
while (true) {
|
|
149
|
+
const input = (await promptText('you> ')).trim();
|
|
150
|
+
if (!input)
|
|
151
|
+
continue;
|
|
152
|
+
if (input === '/exit' || input === '/quit') {
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
if (input === '/help') {
|
|
156
|
+
process.stdout.write('/help 查看帮助\n/threads 列出 threads\n/new 新建 thread\n/use <threadId> 切换 thread\n/model <modelId> 切换模型\n/exit 退出\n\n');
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (input === '/threads') {
|
|
160
|
+
const threads = await ctx.runtime.listThreads(ctx.session, ctx.chatId);
|
|
161
|
+
if (threads.length === 0) {
|
|
162
|
+
process.stdout.write('暂无 threads\n\n');
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
process.stdout.write(`${threads.map((thread) => `- ${ctx.runtime.formatThreadLabel(thread)}`).join('\n')}\n\n`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (input === '/new') {
|
|
169
|
+
threadId = await ctx.runtime.createThread(ctx.session, ctx.chatId, process.cwd(), 'CLI Session');
|
|
170
|
+
process.stdout.write(`已创建 thread: ${threadId}\n\n`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (input.startsWith('/use ')) {
|
|
174
|
+
const nextThreadId = input.slice(5).trim();
|
|
175
|
+
const thread = await ctx.runtime.loadThread(ctx.session, nextThreadId);
|
|
176
|
+
if (!thread) {
|
|
177
|
+
process.stdout.write(`未找到 thread: ${nextThreadId}\n\n`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
threadId = nextThreadId;
|
|
181
|
+
process.stdout.write(`已切换到 thread: ${threadId}\n\n`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (input.startsWith('/model ')) {
|
|
185
|
+
model = input.slice(7).trim() || undefined;
|
|
186
|
+
process.stdout.write(`当前模型: ${model || DEFAULT_LINX_CLOUD_MODEL_ID}\n\n`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
await runSingleTurn({ ctx, threadId, model, prompt: input });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function runPiCommand(argv) {
|
|
193
|
+
const backend = argv.backend ?? 'cloud';
|
|
194
|
+
if (!argv.print && backend === 'cloud') {
|
|
195
|
+
const { resolveLinxPiCloudOAuthCredential } = await import('./lib/pi-adapter/auth.js');
|
|
196
|
+
const existingCredential = await resolveLinxPiCloudOAuthCredential(undefined);
|
|
197
|
+
if (!existingCredential) {
|
|
198
|
+
const answer = (await promptText('LinX Cloud not connected. Open browser login now? [Y/n] ')).trim().toLowerCase();
|
|
199
|
+
const shouldLoginNow = answer === '' || answer === 'y' || answer === 'yes';
|
|
200
|
+
if (shouldLoginNow) {
|
|
201
|
+
const { ensureBrowserConsentLogin } = await import('./lib/oidc-auth.js');
|
|
202
|
+
process.stdout.write('Opening LinX Cloud login in your browser...\n');
|
|
203
|
+
try {
|
|
204
|
+
const result = await ensureBrowserConsentLogin({
|
|
205
|
+
issuerUrl: resolveAccountBaseUrl(),
|
|
206
|
+
});
|
|
207
|
+
if (result.reusedExistingSession) {
|
|
208
|
+
process.stdout.write('Reused existing LinX Cloud session.\n');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
process.stdout.write('LinX Cloud login was not completed. Continuing into TUI without auth.\n');
|
|
213
|
+
if (error instanceof Error && error.message.trim()) {
|
|
214
|
+
process.stdout.write(`${error.message}\n`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const adapter = createPiRuntimeAdapter({
|
|
221
|
+
createNativeProxy(options) {
|
|
222
|
+
return createCodexNativeProxy({
|
|
223
|
+
cwd: options?.cwd,
|
|
224
|
+
model: options?.model,
|
|
225
|
+
listenPort: options?.listenPort,
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
async createRemoteCompletion(options) {
|
|
229
|
+
const chatApi = await import('./lib/chat-api.js');
|
|
230
|
+
return chatApi.createRemoteCompletionResult(options);
|
|
231
|
+
},
|
|
232
|
+
async listRemoteModels(session, runtimeUrl, apiKey) {
|
|
233
|
+
const chatApi = await import('./lib/chat-api.js');
|
|
234
|
+
return chatApi.listRemoteModels(session, runtimeUrl, apiKey);
|
|
235
|
+
},
|
|
236
|
+
}, {
|
|
237
|
+
cwd: argv.cwd || process.cwd(),
|
|
238
|
+
model: argv.model || DEFAULT_LINX_CLOUD_MODEL_ID,
|
|
239
|
+
backend,
|
|
240
|
+
port: argv.port,
|
|
241
|
+
providerConfig: {
|
|
242
|
+
baseUrl: String(argv['runtime-url'] ?? 'https://api.undefineds.co/v1'),
|
|
243
|
+
issuerUrl: resolveAccountBaseUrl(),
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
await adapter.start();
|
|
247
|
+
const { SessionManager } = await import('@mariozechner/pi-coding-agent');
|
|
248
|
+
const runtime = await adapter.createRuntime({
|
|
249
|
+
cwd: adapter.cwd,
|
|
250
|
+
agentDir: LINX_AGENT_DIR,
|
|
251
|
+
sessionManager: SessionManager.inMemory(adapter.cwd),
|
|
252
|
+
});
|
|
253
|
+
const interactive = bootstrapPiInteractiveMode(runtime);
|
|
254
|
+
try {
|
|
255
|
+
if (argv.print) {
|
|
256
|
+
const prompt = (argv.prompt ?? []).join(' ').trim();
|
|
257
|
+
const exitCode = await runPrintMode(runtime, {
|
|
258
|
+
mode: 'text',
|
|
259
|
+
initialMessage: prompt || undefined,
|
|
260
|
+
});
|
|
261
|
+
if (exitCode !== 0) {
|
|
262
|
+
process.exitCode = exitCode;
|
|
263
|
+
}
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
await interactive.run();
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
interactive.stop();
|
|
270
|
+
await adapter.close();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function buildPiCommand(command) {
|
|
274
|
+
const configured = command
|
|
275
|
+
.option('cwd', {
|
|
276
|
+
type: 'string',
|
|
277
|
+
describe: 'Workspace path for the Pi session',
|
|
278
|
+
})
|
|
279
|
+
.option('model', {
|
|
280
|
+
type: 'string',
|
|
281
|
+
describe: 'Model id to expose through the Pi runtime adapter',
|
|
282
|
+
})
|
|
283
|
+
.option('backend', {
|
|
284
|
+
type: 'string',
|
|
285
|
+
default: 'cloud',
|
|
286
|
+
choices: ['cloud', 'native'],
|
|
287
|
+
describe: 'Backend mode. Default is cloud; native keeps the local Codex proxy for debugging only.',
|
|
288
|
+
})
|
|
289
|
+
.option('port', {
|
|
290
|
+
type: 'number',
|
|
291
|
+
default: 8787,
|
|
292
|
+
describe: 'Local websocket port used only when --backend native',
|
|
293
|
+
})
|
|
294
|
+
.option('runtime-url', {
|
|
295
|
+
type: 'string',
|
|
296
|
+
default: 'https://api.undefineds.co/v1',
|
|
297
|
+
describe: 'Cloud runtime API base URL',
|
|
298
|
+
})
|
|
299
|
+
.option('print', {
|
|
300
|
+
type: 'boolean',
|
|
301
|
+
default: false,
|
|
302
|
+
describe: 'Run a single prompt without entering interactive mode',
|
|
303
|
+
})
|
|
304
|
+
.positional('prompt', {
|
|
305
|
+
array: true,
|
|
306
|
+
type: 'string',
|
|
307
|
+
describe: 'Single-shot prompt when --print is enabled',
|
|
308
|
+
});
|
|
309
|
+
return configured;
|
|
310
|
+
}
|
|
311
|
+
const defaultPiCommand = {
|
|
312
|
+
command: '$0 [prompt..]',
|
|
313
|
+
describe: 'Run the native Pi TUI on top of the LinX cloud auth + Pod storage backend',
|
|
314
|
+
builder: buildPiCommand,
|
|
315
|
+
handler: runPiCommand,
|
|
316
|
+
};
|
|
317
|
+
const hiddenPiAliasCommand = {
|
|
318
|
+
command: 'pi [prompt..]',
|
|
319
|
+
describe: false,
|
|
320
|
+
builder: buildPiCommand,
|
|
321
|
+
handler: runPiCommand,
|
|
322
|
+
};
|
|
323
|
+
const hiddenPiFrontendAliasCommand = {
|
|
324
|
+
command: 'pi-frontend [prompt..]',
|
|
325
|
+
describe: false,
|
|
326
|
+
builder: buildPiCommand,
|
|
327
|
+
handler: runPiCommand,
|
|
328
|
+
};
|
|
329
|
+
const execCommand = {
|
|
330
|
+
command: 'exec [prompt..]',
|
|
331
|
+
aliases: ['e'],
|
|
332
|
+
describe: 'Run LinX non-interactively',
|
|
333
|
+
builder: buildPiCommand,
|
|
334
|
+
async handler(argv) {
|
|
335
|
+
await runPiCommand({ ...argv, print: true });
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
const cli = yargs(hideBin(process.argv))
|
|
339
|
+
.scriptName('linx')
|
|
340
|
+
.parserConfiguration({
|
|
341
|
+
'populate--': true,
|
|
342
|
+
})
|
|
343
|
+
.command(loginCommand)
|
|
344
|
+
.command(logoutCommand)
|
|
345
|
+
.command(whoamiCommand)
|
|
346
|
+
.command(aiCommand)
|
|
347
|
+
.command(execCommand)
|
|
348
|
+
.command(defaultPiCommand)
|
|
349
|
+
.command('chat [prompt..]', false, (command) => command
|
|
350
|
+
.option('model', { type: 'string', describe: 'Model ID override' })
|
|
351
|
+
.option('continue', { type: 'boolean', default: false, describe: 'Continue latest thread' })
|
|
352
|
+
.option('thread', { type: 'string', describe: 'Use an existing thread ID' })
|
|
353
|
+
.option('url', { type: 'string', describe: 'Runtime API base URL override' })
|
|
354
|
+
.option('workspace', { type: 'string', describe: 'Workspace/worktree path metadata' }), async (argv) => {
|
|
355
|
+
const ctx = await resolveContext(argv.url);
|
|
356
|
+
const threadId = await resolveThreadId({
|
|
357
|
+
ctx,
|
|
358
|
+
continueMode: argv.continue,
|
|
359
|
+
explicitThreadId: argv.thread,
|
|
360
|
+
workspace: argv.workspace,
|
|
361
|
+
});
|
|
362
|
+
const prompt = argv.prompt?.join(' ').trim() || undefined;
|
|
363
|
+
if (prompt) {
|
|
364
|
+
await runSingleTurn({ ctx, threadId, model: argv.model, prompt });
|
|
365
|
+
await ctx.session.logout();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
await runInteractive({ ctx, initialThreadId: threadId, initialModel: argv.model });
|
|
369
|
+
await ctx.session.logout();
|
|
370
|
+
})
|
|
371
|
+
.command('models', 'List available remote models', (command) => command.option('url', { type: 'string', describe: 'Runtime API base URL override' }), async (argv) => {
|
|
372
|
+
const ctx = await resolveRuntimeAuthContext(argv.url);
|
|
373
|
+
let models;
|
|
374
|
+
try {
|
|
375
|
+
models = await ctx.runtime.listRemoteModels(ctx.session, ctx.runtimeUrl, ctx.apiKey, { fallback: false });
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
379
|
+
throw new Error(`Failed to load cloud models from ${ctx.runtimeUrl}: ${message}`);
|
|
380
|
+
}
|
|
381
|
+
if (models.length === 0) {
|
|
382
|
+
process.stdout.write(`Cloud runtime returned an empty model list.\n`);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
for (const model of models) {
|
|
386
|
+
const meta = [model.provider || model.ownedBy, model.contextWindow ? `${model.contextWindow}` : '']
|
|
387
|
+
.filter(Boolean)
|
|
388
|
+
.join(' · ');
|
|
389
|
+
process.stdout.write(`- ${model.id}${meta ? ` (${meta})` : ''}\n`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
await ctx.session.logout();
|
|
393
|
+
})
|
|
394
|
+
.command('resume', 'List resumable CLI threads', (command) => command.option('url', { type: 'string', describe: 'Runtime API base URL override' }), async (argv) => {
|
|
395
|
+
const ctx = await resolveContext(argv.url);
|
|
396
|
+
const threads = await ctx.runtime.listThreads(ctx.session, ctx.chatId);
|
|
397
|
+
if (threads.length === 0) {
|
|
398
|
+
process.stdout.write('No threads found.\n');
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
process.stdout.write(`${threads.map((thread) => `- ${ctx.runtime.formatThreadLabel(thread)}`).join('\n')}\n`);
|
|
402
|
+
}
|
|
403
|
+
await ctx.session.logout();
|
|
404
|
+
})
|
|
405
|
+
.command('fork [thread]', 'Fork a previous interactive session', (command) => command
|
|
406
|
+
.positional('thread', { type: 'string', describe: 'Thread ID to fork' })
|
|
407
|
+
.option('last', { type: 'boolean', default: false, describe: 'Fork the most recent thread' }), () => {
|
|
408
|
+
throw new Error('Fork is not implemented yet for LinX Pod-backed Pi sessions.');
|
|
409
|
+
})
|
|
410
|
+
.command(hiddenPiAliasCommand)
|
|
411
|
+
.command(hiddenPiFrontendAliasCommand)
|
|
412
|
+
.command('codex-native-proxy', 'Start a local app-server websocket proxy for native Codex TUI', (command) => command
|
|
413
|
+
.option('cwd', {
|
|
414
|
+
type: 'string',
|
|
415
|
+
describe: 'Workspace path exposed to the native Codex shell',
|
|
416
|
+
})
|
|
417
|
+
.option('model', {
|
|
418
|
+
type: 'string',
|
|
419
|
+
describe: 'Model override forwarded to the native proxy session metadata',
|
|
420
|
+
})
|
|
421
|
+
.option('port', {
|
|
422
|
+
type: 'number',
|
|
423
|
+
default: 8787,
|
|
424
|
+
describe: 'Local websocket listen port for codex --remote',
|
|
425
|
+
}), async (argv) => {
|
|
426
|
+
const proxy = createCodexNativeProxy({
|
|
427
|
+
cwd: argv.cwd || process.cwd(),
|
|
428
|
+
model: argv.model,
|
|
429
|
+
listenPort: argv.port,
|
|
430
|
+
});
|
|
431
|
+
await proxy.start();
|
|
432
|
+
process.stdout.write(`[linx] native codex proxy ready\n`);
|
|
433
|
+
process.stdout.write(`[linx] connect with: codex --remote ${proxy.remoteUrl} -C ${proxy.record.cwd}\n`);
|
|
434
|
+
const shutdown = async () => {
|
|
435
|
+
await proxy.close();
|
|
436
|
+
process.exit(0);
|
|
437
|
+
};
|
|
438
|
+
process.on('SIGINT', () => {
|
|
439
|
+
void shutdown();
|
|
440
|
+
});
|
|
441
|
+
process.on('SIGTERM', () => {
|
|
442
|
+
void shutdown();
|
|
443
|
+
});
|
|
444
|
+
await new Promise(() => { });
|
|
445
|
+
})
|
|
446
|
+
.command('watch <action> [backend] [prompt..]', 'Run or inspect local watch backends', (command) => command
|
|
447
|
+
.positional('action', {
|
|
448
|
+
type: 'string',
|
|
449
|
+
choices: ['run', 'backends', 'sessions', 'show', 'approvals', 'approve', 'reject', 'codex', 'claude', 'codebuddy'],
|
|
450
|
+
})
|
|
451
|
+
.positional('backend', {
|
|
452
|
+
type: 'string',
|
|
453
|
+
describe: 'Watch backend for `run`, session id for `show`, or approval id for `approve|reject`',
|
|
454
|
+
})
|
|
455
|
+
.option('mode', {
|
|
456
|
+
type: 'string',
|
|
457
|
+
default: 'smart',
|
|
458
|
+
choices: ['manual', 'smart', 'auto'],
|
|
459
|
+
describe: 'Unified autonomy mode',
|
|
460
|
+
})
|
|
461
|
+
.option('model', {
|
|
462
|
+
type: 'string',
|
|
463
|
+
describe: 'Backend-native model override',
|
|
464
|
+
})
|
|
465
|
+
.option('cwd', {
|
|
466
|
+
type: 'string',
|
|
467
|
+
describe: 'Working directory for local backend execution',
|
|
468
|
+
})
|
|
469
|
+
.option('plain', {
|
|
470
|
+
type: 'boolean',
|
|
471
|
+
default: false,
|
|
472
|
+
describe: 'Disable full-screen TUI and use plain streaming output',
|
|
473
|
+
})
|
|
474
|
+
.option('credential-source', {
|
|
475
|
+
type: 'string',
|
|
476
|
+
default: 'auto',
|
|
477
|
+
choices: ['auto', 'local', 'cloud'],
|
|
478
|
+
describe: 'Resolve credentials only: local CLI login, LinX cloud config, or auto fallback. Runtime still runs locally.',
|
|
479
|
+
})
|
|
480
|
+
.option('session', {
|
|
481
|
+
type: 'boolean',
|
|
482
|
+
default: false,
|
|
483
|
+
describe: 'Approve for the current watch session instead of only once.',
|
|
484
|
+
})
|
|
485
|
+
.option('reason', {
|
|
486
|
+
type: 'string',
|
|
487
|
+
describe: 'Optional note recorded with an approval decision.',
|
|
488
|
+
}), async (argv) => {
|
|
489
|
+
const rawAction = String(argv.action);
|
|
490
|
+
const directBackend = ['codex', 'claude', 'codebuddy'].includes(rawAction);
|
|
491
|
+
const action = directBackend ? 'run' : rawAction;
|
|
492
|
+
if (action === 'backends') {
|
|
493
|
+
const backends = listSupportedWatchBackends();
|
|
494
|
+
for (const backend of backends) {
|
|
495
|
+
process.stdout.write(`- ${backend.backend} (${backend.label})\n`);
|
|
496
|
+
process.stdout.write(` ${backend.description}\n`);
|
|
497
|
+
process.stdout.write(` manual: ${backend.modes.manual}\n`);
|
|
498
|
+
process.stdout.write(` smart: ${backend.modes.smart}\n`);
|
|
499
|
+
process.stdout.write(` auto: ${backend.modes.auto}\n`);
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (action === 'sessions') {
|
|
504
|
+
const sessions = listArchivedWatchSessions();
|
|
505
|
+
if (sessions.length === 0) {
|
|
506
|
+
process.stdout.write('No watch sessions found.\n');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
process.stdout.write(`${sessions.map(formatWatchSessionSummary).join('\n')}\n`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (action === 'show') {
|
|
513
|
+
const sessionId = argv.backend ? String(argv.backend) : '';
|
|
514
|
+
if (!sessionId) {
|
|
515
|
+
throw new Error('Usage: linx watch show <sessionId>');
|
|
516
|
+
}
|
|
517
|
+
const session = loadArchivedWatchSession(sessionId);
|
|
518
|
+
if (!session) {
|
|
519
|
+
throw new Error(`Watch session not found: ${sessionId}`);
|
|
520
|
+
}
|
|
521
|
+
process.stdout.write(formatArchivedWatchSession(session, loadArchivedWatchEvents(sessionId)));
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (action === 'approvals') {
|
|
525
|
+
const approvals = await listRemoteWatchApprovals();
|
|
526
|
+
if (approvals.length === 0) {
|
|
527
|
+
process.stdout.write('No pending remote approvals in the approval inbox.\n');
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
process.stdout.write(`${approvals.map(formatRemoteWatchApprovalSummary).join('\n')}\n`);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (action === 'approve' || action === 'reject') {
|
|
534
|
+
const approvalId = argv.backend ? String(argv.backend) : '';
|
|
535
|
+
if (!approvalId) {
|
|
536
|
+
throw new Error(`Usage: linx watch ${action} <approvalId>`);
|
|
537
|
+
}
|
|
538
|
+
const summary = await resolveRemoteWatchApproval({
|
|
539
|
+
approvalId,
|
|
540
|
+
decision: action === 'approve'
|
|
541
|
+
? (argv.session ? 'accept_for_session' : 'accept')
|
|
542
|
+
: 'decline',
|
|
543
|
+
note: argv.reason ? String(argv.reason) : undefined,
|
|
544
|
+
});
|
|
545
|
+
process.stdout.write(`${formatRemoteWatchApprovalSummary(summary)}\n`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const backend = (directBackend ? rawAction : argv.backend);
|
|
549
|
+
if (!backend || !['codex', 'claude', 'codebuddy'].includes(backend)) {
|
|
550
|
+
throw new Error('Usage: linx watch run <codex|claude|codebuddy> <prompt> [-- backend args]\n or: linx watch <codex|claude|codebuddy> <prompt>');
|
|
551
|
+
}
|
|
552
|
+
const plain = Boolean(argv.plain);
|
|
553
|
+
const prompt = ((directBackend ? [argv.backend, ...(argv.prompt ?? [])] : argv.prompt) ?? [])
|
|
554
|
+
.filter((item) => typeof item === 'string')
|
|
555
|
+
.join(' ')
|
|
556
|
+
.trim() || undefined;
|
|
557
|
+
const exitCode = await runWatch({
|
|
558
|
+
backend,
|
|
559
|
+
mode: argv.mode,
|
|
560
|
+
cwd: argv.cwd || process.cwd(),
|
|
561
|
+
plain,
|
|
562
|
+
model: argv.model,
|
|
563
|
+
prompt,
|
|
564
|
+
passthroughArgs: (argv['--'] ?? []).map(String),
|
|
565
|
+
credentialSource: argv['credential-source'],
|
|
566
|
+
});
|
|
567
|
+
if (exitCode !== 0) {
|
|
568
|
+
process.exitCode = exitCode;
|
|
569
|
+
}
|
|
570
|
+
})
|
|
571
|
+
.strict()
|
|
572
|
+
.help();
|
|
573
|
+
process.on('unhandledRejection', (error) => {
|
|
574
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
575
|
+
process.exit(1);
|
|
576
|
+
});
|
|
577
|
+
cli.parse();
|
|
578
|
+
//# sourceMappingURL=index.js.map
|