linkitty 0.0.0-stage → 0.17.3

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 (33) hide show
  1. package/README.md +89 -2
  2. package/README.zh-CN.md +90 -0
  3. package/THIRD_PARTY_NOTICES.md +5 -0
  4. package/THIRD_PARTY_NOTICES.zh-CN.md +5 -0
  5. package/artifacts.json +10 -0
  6. package/bin/linkitty.cjs +195 -0
  7. package/lib/i18n.cjs +31 -0
  8. package/lib/install.cjs +109 -0
  9. package/lib/messages.json +487 -0
  10. package/node_modules/qrcode-terminal/.travis.yml +3 -0
  11. package/node_modules/qrcode-terminal/LICENSE +222 -0
  12. package/node_modules/qrcode-terminal/README.md +82 -0
  13. package/node_modules/qrcode-terminal/bin/qrcode-terminal.js +101 -0
  14. package/node_modules/qrcode-terminal/example/basic.js +2 -0
  15. package/node_modules/qrcode-terminal/example/basic.png +0 -0
  16. package/node_modules/qrcode-terminal/example/callback.js +4 -0
  17. package/node_modules/qrcode-terminal/example/small-qrcode.js +6 -0
  18. package/node_modules/qrcode-terminal/lib/main.js +100 -0
  19. package/node_modules/qrcode-terminal/package.json +37 -0
  20. package/node_modules/qrcode-terminal/test/main.js +63 -0
  21. package/node_modules/qrcode-terminal/vendor/QRCode/QR8bitByte.js +22 -0
  22. package/node_modules/qrcode-terminal/vendor/QRCode/QRBitBuffer.js +38 -0
  23. package/node_modules/qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js +7 -0
  24. package/node_modules/qrcode-terminal/vendor/QRCode/QRMaskPattern.js +10 -0
  25. package/node_modules/qrcode-terminal/vendor/QRCode/QRMath.js +44 -0
  26. package/node_modules/qrcode-terminal/vendor/QRCode/QRMode.js +6 -0
  27. package/node_modules/qrcode-terminal/vendor/QRCode/QRPolynomial.js +66 -0
  28. package/node_modules/qrcode-terminal/vendor/QRCode/QRRSBlock.js +298 -0
  29. package/node_modules/qrcode-terminal/vendor/QRCode/QRUtil.js +272 -0
  30. package/node_modules/qrcode-terminal/vendor/QRCode/index.js +462 -0
  31. package/package.json +30 -4
  32. package/payload/darwin-arm64.tgz +0 -0
  33. package/payload/linux-x64.tgz +0 -0
package/README.md CHANGED
@@ -1,3 +1,90 @@
1
- # Temporary Holding Version
1
+ # Linkitty CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ English · [简体中文](README.zh-CN.md)
4
+
5
+ Connect agents, local websites and APIs from your computer. Download the CLI tarball from the [official website](https://linkitty.spark-data.cn/?lang=en#downloads). On macOS, install the matching desktop tarball too; Linux installs only the CLI. Node.js 22+ is required.
6
+
7
+ ## Published npm 0.17.0 and upgrades
8
+
9
+ The immutable CLI 0.17.0 package is published for macOS ARM64 and Linux x64. Its recorded npm installation acceptance covers macOS ARM64; see acceptance and limits. Node.js 22+ is required.
10
+
11
+ ```sh
12
+ npm install -g linkitty@0.17.0
13
+ ```
14
+
15
+ For an existing installation upgraded with npm12, rebuild the verified runtime after installing:
16
+
17
+ ```sh
18
+ npm rebuild -g linkitty --allow-scripts=linkitty
19
+ ```
20
+
21
+ Keep the same custom `--prefix` for both commands. macOS Apple Silicon can also install `linkitty-desktop@0.17.0`; Linux installs only the CLI. New source features require a matching verified release. Use a matching verified release for candidate-only features. The [download page](https://linkitty.spark-data.cn/?lang=en#downloads) also provides matching tarballs.
22
+
23
+ ## Connect
24
+
25
+ ```sh
26
+ linkitty --lang en
27
+ linkitty --region cn
28
+ linkitty login
29
+ linkitty connect codex
30
+ linkitty connect pi --executable /absolute/path/to/pi
31
+ linkitty connect claude --profile /absolute/private/claude/profile.json
32
+ linkitty connect dsh --profile /absolute/private/deepseek/profile.json
33
+ linkitty connect web 127.0.0.1:8080 --name Workbench
34
+ linkitty connect api 127.0.0.1:9000 --routes /absolute/api-routes.json
35
+ ```
36
+
37
+ All `connect` commands accept `-qr` for phone claiming. Omit it when signed in to claim directly. No project is bound by default; add `--workspace /absolute/project` when needed. Pi may use `--agent-dir` for its existing authentication directory. DSH means DeepSeek Harness. Each agent keeps its own model login, tool permissions and approvals; Linkitty does not turn it into a Codex sandbox.
38
+
39
+ Codex desktop sharing supports macOS only. Before first connection, finish tasks and quit independently running Codex. Reopen it normally after connecting; no daily shared-control launcher is required. Background startup honors enabled macOS system proxies, with existing proxy environment variables taking precedence.
40
+
41
+ Qoder CLI 1.1.37 uses its own sign-in. With Node.js 22+, npm and jq installed, the first connection prepares its SDK automatically:
42
+
43
+ ```sh
44
+ linkitty connect qoder
45
+ ```
46
+
47
+ Use a matching Connector and Coding plugin that declare `relay.qoder`. This local integration does not update published packages. Qoder keeps its official local sign-in; its profile must not contain a model key or custom endpoint. IDE shared control is unavailable.
48
+
49
+ ## Manage connections
50
+
51
+ ```sh
52
+ linkitty status
53
+ linkitty list
54
+ linkitty disconnect ID
55
+ linkitty service status
56
+ linkitty service uninstall
57
+ linkitty delete INSTANCE_ID --yes
58
+ ```
59
+
60
+ The CLI and desktop share one user background service. Closing a terminal or window leaves it running. Sleep, network loss or a stopped business application makes the service unavailable. `disconnect` pauses a connection; uninstalling the background service retains configuration. `delete` permanently removes service registration and sharing grants, retaining projects, native sessions and computer files.
61
+
62
+ ## API access
63
+
64
+ The route file declares a business contract and explicit capabilities:
65
+
66
+ ```json
67
+ {
68
+ "business_api": {"id": "example.api", "version": "1.0.0"},
69
+ "routes": [
70
+ {"methods": ["GET"], "path": "/health", "requires": ["api.read"]},
71
+ {"methods": ["POST"], "path": "/jobs", "requires": ["api.write"]}
72
+ ]
73
+ }
74
+ ```
75
+
76
+ The upstream is fixed to a loopback HTTP origin. HTTP/SSE keep business paths; WebSocket routes require `websocket: true`. Unknown routes and missing capabilities are rejected. Changing the permission file creates a new instance; pause the old one explicitly.
77
+
78
+ ```sh
79
+ linkitty access INSTANCE_ID --capabilities api.read --output /absolute/private/access.json
80
+ ```
81
+
82
+ The mode-600 file contains a short-lived `endpoint` and `access_token`; requests use `Relay-Authorization: Bearer <access_token>`. Never print, commit or share this file. Create a new file after expiry; existing files are not overwritten. See the [online developer guide](https://github.com/lingzhi-wlz/relay-gateway/blob/main/docs/third-party-developer.md) for stream handling and plugin integration.
83
+
84
+ ## Platforms, region and language
85
+
86
+ Payloads support macOS ARM64 and Linux x64; installation selects the platform and verifies its hash. Linux targets Ubuntu 24.04 / glibc 2.39 and needs user-level systemd. Binaries/installers were checked in containers; real user-systemd startup remains unverified. Windows uses WSL 2 / Ubuntu 24.04 and is not verified on a real machine. Native Windows, Linux ARM64 and musl have no payload. Use Pi, Claude or DSH on Linux/WSL.
87
+
88
+ China is the available account region. Singapore and US require their own configured endpoints; switching regions does not migrate accounts or services. `linkitty --lang zh|en|ja|ru` saves a preference; `LINKITTY_LANG` overrides the current process. Language does not change region.
89
+
90
+ Desktop and CLI share `~/.linkitty/`. Keep configuration and credentials protected, and never package private data. Platform login and agent model login are separate.
@@ -0,0 +1,90 @@
1
+ # Linkitty CLI
2
+
3
+ [English](README.md) · 简体中文
4
+
5
+ 接入电脑上的 Agent、本机网站和 API。从[官网](https://linkitty.spark-data.cn/?lang=zh#downloads)下载 CLI tarball;macOS 同时安装匹配桌面包,Linux 只装 CLI。需要 Node.js 22+。
6
+
7
+ ## 已发布的 npm 0.17.0 与升级
8
+
9
+ CLI 0.17.0 不可变包已发布,包含 macOS ARM64 与 Linux x64;该发行记录的真实 npm 安装验收覆盖 macOS ARM64,见验收与边界。需要 Node.js 22+。
10
+
11
+ ```sh
12
+ npm install -g linkitty@0.17.0
13
+ ```
14
+
15
+ 已有安装使用 npm12 升级后,重建已验证的运行组件:
16
+
17
+ ```sh
18
+ npm rebuild -g linkitty --allow-scripts=linkitty
19
+ ```
20
+
21
+ 两条命令的自定义 `--prefix` 必须相同。macOS Apple Silicon 可另装 `linkitty-desktop@0.17.0`,Linux 只装 CLI。新源码功能须使用相匹配的已验证发行;候选功能须使用相匹配的已验证发行。[官网下载页](https://linkitty.spark-data.cn/?lang=zh#downloads)继续提供匹配tarball。
22
+
23
+ ## 接入
24
+
25
+ ```sh
26
+ linkitty --lang zh
27
+ linkitty --region cn
28
+ linkitty login
29
+ linkitty connect codex
30
+ linkitty connect pi --executable /absolute/path/to/pi
31
+ linkitty connect claude --profile /absolute/private/claude/profile.json
32
+ linkitty connect dsh --profile /absolute/private/deepseek/profile.json
33
+ linkitty connect web 127.0.0.1:8080 --name Workbench
34
+ linkitty connect api 127.0.0.1:9000 --routes /absolute/api-routes.json
35
+ ```
36
+
37
+ 所有 `connect` 命令可加 `-qr` 由手机扫码;已登录时省略即可直接认领。不默认绑定项目,需要时加 `--workspace /absolute/project`。Pi 可用 `--agent-dir` 指定既有认证目录。DSH 指 DeepSeek Harness。各 Agent 保留自身模型登录、工具权限和审批,Linkitty 不会把它变成 Codex 沙箱。
38
+
39
+ Codex 桌面共用仅支持 macOS。首次接入前完成任务并退出独立运行的 Codex,接入成功后正常打开,无需每日运行共控脚本。后台启动使用已启用的 macOS 系统代理,既有代理环境变量优先。
40
+
41
+ Qoder CLI 1.1.37 使用自身登录。安装 Node.js 22+、npm 和 jq 后,首次接入会自动准备 SDK:
42
+
43
+ ```sh
44
+ linkitty connect qoder
45
+ ```
46
+
47
+ 需要声明 `relay.qoder` 的匹配 Connector 与 Coding 插件,本地接入不会自动更新已发布包。Qoder 保留自身官方登录,配置不得包含模型 Key 或自定义端点,未接入 IDE 共控。
48
+
49
+ ## 管理连接
50
+
51
+ ```sh
52
+ linkitty status
53
+ linkitty list
54
+ linkitty disconnect ID
55
+ linkitty service status
56
+ linkitty service uninstall
57
+ linkitty delete INSTANCE_ID --yes
58
+ ```
59
+
60
+ CLI 与桌面共用一个用户后台服务,关闭终端或窗口后继续运行。电脑休眠、断网或业务程序停止时不可用。`disconnect` 暂停连接;卸载后台服务保留配置;`delete` 永久删除服务登记及分享授权,保留项目、原生会话和电脑文件。
61
+
62
+ ## API 访问
63
+
64
+ 路由文件声明业务契约和明确能力:
65
+
66
+ ```json
67
+ {
68
+ "business_api": {"id": "example.api", "version": "1.0.0"},
69
+ "routes": [
70
+ {"methods": ["GET"], "path": "/health", "requires": ["api.read"]},
71
+ {"methods": ["POST"], "path": "/jobs", "requires": ["api.write"]}
72
+ ]
73
+ }
74
+ ```
75
+
76
+ 上游固定为 loopback HTTP 地址。HTTP/SSE 保留业务路径;WebSocket 路由需加 `websocket: true`。未知路由和缺失能力均拒绝。权限文件变化会形成新实例,旧连接需显式暂停。
77
+
78
+ ```sh
79
+ linkitty access INSTANCE_ID --capabilities api.read --output /absolute/private/access.json
80
+ ```
81
+
82
+ 600 权限文件包含短期 `endpoint` 和 `access_token`,请求使用 `Relay-Authorization: Bearer <access_token>`。不打印、提交或分享该文件;到期后另建,不能覆盖已有文件。流处理和插件接入见[在线开发者指南](https://github.com/lingzhi-wlz/relay-gateway/blob/main/docs/third-party-developer.zh-CN.md)。
83
+
84
+ ## 平台、地域和语言
85
+
86
+ 安装包支持 macOS ARM64、Linux x64,安装时选择平台并校验摘要。Linux 面向 Ubuntu 24.04 / glibc 2.39,需要用户级 systemd。二进制与安装器已在容器检查,真实用户 systemd 启动仍未验证。Windows 使用 WSL 2 / Ubuntu 24.04,尚未实机验证。没有原生 Windows、Linux ARM64 或 musl 制品。Linux/WSL 使用 Pi、Claude 或 DSH。
87
+
88
+ 中国区可用;新加坡和美国需另行配置入口,切换地域不迁移账号或服务。`linkitty --lang zh|en|ja|ru` 保存语言,`LINKITTY_LANG` 覆盖当前进程;语言不改变地域。
89
+
90
+ 桌面与 CLI 共用 `~/.linkitty/`。配置与凭据须受保护,不打包私人目录。平台登录与模型登录各自独立。
@@ -0,0 +1,5 @@
1
+ # Third-party software
2
+
3
+ [简体中文](THIRD_PARTY_NOTICES.zh-CN.md)
4
+
5
+ Native components use Rust/iroh, Flutter and their dependencies. Their licenses are unchanged by the Linkitty package name; corresponding notices are included in the payload licenses directory. No open-source license is granted for Linkitty first-party code at this stage.
@@ -0,0 +1,5 @@
1
+ # 第三方软件
2
+
3
+ [English](THIRD_PARTY_NOTICES.md)
4
+
5
+ 原生组件使用 Rust/iroh、Flutter 及各自依赖,许可证不因 Linkitty 包名改变。制品内 licenses 目录保留对应通知。Linkitty 自有代码未另行授予开源许可。
package/artifacts.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "darwin-arm64": {
3
+ "archive": "payload/darwin-arm64.tgz",
4
+ "sha256": "da2b03361978234a731b9a7f681fd374e5faedca490485e8c5bc5be9307d6787"
5
+ },
6
+ "linux-x64": {
7
+ "archive": "payload/linux-x64.tgz",
8
+ "sha256": "63f0332b8bbd91b8f00523613239bd5d99041b8c6eea974f0f299c3a4b17ce2d"
9
+ }
10
+ }
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const {t,setLanguage}=require("../lib/i18n.cjs");
4
+ const fs=require('node:fs');
5
+ const path=require('node:path');
6
+ const os=require('node:os');
7
+ const readline=require('node:readline/promises');
8
+ const {spawnSync}=require('node:child_process');
9
+ const {runtime}=require('../lib/install.cjs');
10
+ const pkg=require('../package.json');
11
+ const help=()=>'Linkitty '+pkg.version+'\n\n'+
12
+ t(' linkitty login 登录中继账号\n')+
13
+ t(' linkitty connect codex [-qr] 接入 Codex,不默认绑定项目;-qr 显示认领码\n')+
14
+ t(' linkitty connect pi|claude|dsh|qoder [-qr] 接入现有 Coding 运行环境\n')+
15
+ t(' linkitty connect web 127.0.0.1:8080 接入本机网站\n')+
16
+ t(' linkitty connect api 地址 --routes 文件 按明确路由接入纯 API\n')+
17
+ t(' linkitty access 实例ID --capabilities api.read --output 文件\n')+
18
+ t(' linkitty --region cn|sg|us 保存默认区域\n')+
19
+ t(' linkitty --lang zh|en|ja|ru 保存界面语言\n')+
20
+ t(' linkitty --region sg --endpoint URL 配置该区域的 HTTPS 平台入口\n')+
21
+ t(' linkitty status | list 查看登录、后台与连接\n')+
22
+ t(' linkitty disconnect ID 暂停一个连接\n')+
23
+ t(' linkitty delete 实例ID --yes 永久删除服务登记和分享授权\n')+
24
+ t(' linkitty logout 退出账号并暂停其连接\n')+
25
+ ' linkitty service install|status|uninstall\n\n'+
26
+ t('通用选项:--region、--endpoint、--json;connect 可用 --workspace、--name。\n')+
27
+ t('Coding 可用 --executable;Claude/DSH 可用 --profile;Pi 可用 --agent-dir。\n')+
28
+ t('login --stdin 从标准输入读取账号密码 JSON。首次连接会安装用户后台服务。\n');
29
+ function parse(argv) {
30
+ const opts={},pos=[];
31
+ const values=new Set(['--lang','--region','--endpoint','--workspace','--name','--username','--adapter','--codex','--executable','--profile','--agent-dir','--routes','--capabilities','--output']);
32
+ for(let i=0;i<argv.length;i++){
33
+ const arg=argv[i];
34
+ if(values.has(arg)){
35
+ const value=argv[++i];if(!value||value.startsWith('--'))throw new Error(arg+t(' 缺少值。'));
36
+ opts[arg.slice(2)]=value;
37
+ }else if(['--json','--stdin','--qr','-qr','--yes'].includes(arg)){opts[arg==='-qr'?'qr':arg.slice(2)]=true;}
38
+ else if(arg.startsWith('-'))throw new Error(t('未知选项:')+arg);
39
+ else pos.push(arg);
40
+ }
41
+ if(opts.region&&!['cn','sg','us'].includes(opts.region))throw new Error(t('区域必须是 cn、sg 或 us。'));
42
+ if(opts.endpoint&&!opts.region)throw new Error(t('--endpoint 需要同时指定 --region。'));
43
+ return {opts,pos};
44
+ }
45
+ function native(args,input) {
46
+ const result=spawnSync(runtime(),args,{input:input===undefined?undefined:JSON.stringify(input),encoding:'utf8',maxBuffer:8*1024*1024,timeout:250000,windowsHide:true});
47
+ if(result.error)throw new Error(result.error.code==='ETIMEDOUT'?t('请求结果待核实,请用 linkitty status 或 list 查看。'):result.error.message);
48
+ let output;
49
+ try{output=JSON.parse(result.stdout);}catch{throw new Error(t('后台操作失败,请运行 linkitty service status 检查。'));}
50
+ if(result.status!==0||output.error)throw new Error(friendly(output.error||'runtime_failed'));
51
+ return output;
52
+ }
53
+ function friendly(code) {
54
+ const messages={
55
+ desktop_environment_conflict:t('已有另一个 Codex 启动连接,本次没有覆盖;请核对已有启动设置。'),
56
+ desktop_startup_registration_failed:t('未能保存启动设置,原有启动环境已保留,请重试。'),
57
+ login_required_or_use_qr:t('请先运行 linkitty login,或添加 -qr 用手机扫码认领。'),
58
+ codex_component_required:t('当前制品缺少 Coding 接入组件,请重新安装 Linkitty。'),
59
+ codex_executable_missing:t('请先安装官方 Codex 桌面版,再重新接入。'),
60
+ desktop_shared_connection_required:t('请先完成当前任务并退出 Codex,再次执行接入命令;接入成功后正常打开 Codex 即可。'),
61
+ codex_shared_platform_unsupported:t('当前 Codex 共享接入仅支持 macOS;此系统不会启动独立执行器冒充共控。'),
62
+ codex_execution_state_unavailable:t('暂时无法确认 Codex 的共享连接状态,请稍后重试;已有会话和服务绑定保留。'),
63
+ shared_runtime_busy:t('其他 Codex 会话或执行器仍占用任务,请完成任务并关闭对应会话后再接入。本次没有启动新执行器。'),
64
+ shared_runtime_state_unknown:t('暂时无法核对已有 Codex 执行器状态,请稍后重试;本次没有启动新执行器。'),
65
+ codex_prepare_failed:t('Coding 接入准备失败,请核对执行器、认证环境和 profile 配置。'),
66
+ harness_profile_required:t('请先配置本机 Claude/DSH 运行环境,再用 --profile 指定受保护的 profile.json。'),
67
+ coding_executable_missing:t('没有找到本机执行器,请用 --executable 指定完整路径。'),
68
+ qoder_executable_missing:t('请先安装 Qoder CLI 并执行 qoder login,再重新接入。'),
69
+ qoder_login_required:t('Qoder 登录已失效,请在电脑执行 qoder login 后重试。'),
70
+ qoder_version_unsupported:t('当前接入支持 Qoder CLI 1.1.37,请核对电脑上的 Qoder 版本。'),
71
+ qoder_setup_component_required:t('当前安装包缺少 Qoder 接入资源,请更新 Linkitty。'),
72
+ qoder_runtime_setup_failed:t('Qoder 运行库准备失败,请确认 Node.js 22+、npm、jq 和网络可用后重试。'),
73
+ qoder_setup_timeout:t('Qoder 运行库准备超时,请检查网络后重新接入。'),
74
+ harness_file_missing:t('运行配置中的执行器、桥接程序或凭据文件不存在。'),
75
+ api_routes_required:t('纯 API 接入需要 --routes 文件,包含 business_api 和 routes。'),
76
+ legacy_connection_running:t('此项目的旧版连接仍在运行。请完成当前任务并停止旧连接,再接入 Linkitty;将复用原实例绑定。'),
77
+ legacy_connection_ambiguous:t('发现多个旧版实例指向此项目,请先在原管理入口确认需要保留的连接。'),
78
+ workspace_access_denied:t('系统未允许后台访问此项目目录。请在系统设置中授予 Linkitty 后台组件目录权限,或选择可访问的项目目录。'),
79
+ workspace_unavailable:t('项目目录不存在或无法读取,请核对 --workspace。'),
80
+ background_service_disabled:t('后台连接已停止。运行 linkitty service install 恢复,或重新执行 connect。'),
81
+ local_service_unreachable:t('没有连接到本机服务,请确认程序已启动且端口正确。'),
82
+ local_request_outcome_unknown:t('请求结果待核实,请查看 linkitty status 或 list;不会自动重发操作。'),
83
+ system_service_command_failed:t('系统后台服务注册失败,请查看交接文档中的系统设置。'),
84
+ application_bound_to_other_account:t('这个服务已属于另一个账号,请使用原拥有者账号。'),
85
+ };
86
+ if(String(code).startsWith('region_endpoint_not_configured:'))return t('该区域尚未配置,请运行 linkitty --region ')+code.split(':')[1]+t(' --endpoint https://你的域名/gateway');
87
+ return messages[code]||String(code);
88
+ }
89
+ async function password() {
90
+ process.stdout.write(t('密码:'));
91
+ const stdin=process.stdin;
92
+ if(!stdin.isTTY)throw new Error(t('非交互登录请使用 login --stdin。'));
93
+ return new Promise((resolve,reject)=>{
94
+ let value='';
95
+ const wasRaw=stdin.isRaw;
96
+ const finish=(error)=>{stdin.off('data',onData);stdin.setRawMode(Boolean(wasRaw));stdin.pause();process.stdout.write('\n');error?reject(error):resolve(value);};
97
+ const onData=(data)=>{
98
+ for(const char of data.toString('utf8')){
99
+ if(char==='\r'||char==='\n'){finish();return;}
100
+ if(char==='\u0003'||char==='\u0004'){finish(new Error(t('登录已取消。')));return;}
101
+ if(char==='\u007f'||char==='\b')value=Array.from(value).slice(0,-1).join('');
102
+ else if(char>=' '&&value.length<4096)value+=char;
103
+ }
104
+ };
105
+ stdin.setRawMode(true);stdin.resume();stdin.on('data',onData);
106
+ });
107
+ }
108
+ function findExecutable(name) {
109
+ for(const dir of (process.env.PATH||'').split(path.delimiter)){
110
+ for(const suffix of process.platform==='win32'?['.exe','.cmd','']:['']){
111
+ const file=path.resolve(dir,name+suffix);
112
+ if(fs.existsSync(file)&&fs.statSync(file).isFile())return fs.realpathSync(file);
113
+ }
114
+ }
115
+ return undefined;
116
+ }
117
+ async function main(argv) {
118
+ const languageIndex=argv.indexOf('--lang');
119
+ if(languageIndex>=0)setLanguage(argv[languageIndex+1]);
120
+ if(argv.includes('--version')||argv.includes('-v')){console.log(pkg.version);return;}
121
+ if(!argv.length||argv.includes('--help')||argv.includes('-h')){console.log(help());return;}
122
+ const {opts,pos}=parse(argv);
123
+ if(opts.lang&&!pos.length&&!opts.region&&!opts.endpoint){console.log(opts.json?JSON.stringify({language:opts.lang}):t('语言已保存:')+opts.lang);return;}
124
+ const request=(op,args={})=>native(['request'],{op,args,region:opts.region});
125
+ if(opts.endpoint||!pos.length){
126
+ const result=request('region',{region:opts.region,endpoint:opts.endpoint});
127
+ if(!pos.length){console.log(opts.json?JSON.stringify(result):t('默认区域:')+result.selected);return;}
128
+ }
129
+ const command=pos[0];let result;
130
+ switch(command){
131
+ case 'login': {
132
+ if(pos.length!==1)throw new Error(t('用法:linkitty login'));
133
+ let credentials;
134
+ if(opts.stdin){
135
+ const bytes=Buffer.alloc(65537);let count=0;
136
+ while(count<bytes.length){const n=fs.readSync(0,bytes,count,bytes.length-count,null);if(n===0)break;count+=n;}
137
+ if(count>65536)throw new Error(t('登录输入过大。'));
138
+ try{credentials=JSON.parse(bytes.subarray(0,count).toString('utf8'));}catch{throw new Error(t('登录输入不是有效的 JSON。'));}
139
+ }else{
140
+ if(!process.stdin.isTTY)throw new Error(t('非交互登录请使用 login --stdin。'));
141
+ let username=opts.username;
142
+ if(!username){const rl=readline.createInterface({input:process.stdin,output:process.stdout});username=await rl.question(t('账号:'));rl.close();}
143
+ credentials={username,password:await password()};
144
+ }
145
+ if(typeof credentials.username!=='string'||typeof credentials.password!=='string')throw new Error(t('需要 username 和 password。'));
146
+ result=request('login',credentials);break;
147
+ }
148
+ case 'connect': {
149
+ if(!['codex','pi','claude','dsh','qoder','web','api'].includes(pos[1]))throw new Error(t('接入类型:codex、pi、claude、dsh、qoder、web、api。'));
150
+ const proxy=['web','api'].includes(pos[1]);
151
+ if(proxy&&(!pos[2]||pos.length!==3))throw new Error(t('请提供本机地址,例如 127.0.0.1:8080。'));
152
+ if(!proxy&&pos.length!==2)throw new Error(t('请使用 --workspace 指定项目目录。'));
153
+ let routes;
154
+ if(pos[1]==='api'){
155
+ if(!opts.routes)throw new Error(t('纯 API 接入需要 --routes 权限文件。'));
156
+ const file=path.resolve(opts.routes);
157
+ if(fs.statSync(file).size>128*1024)throw new Error(t('路由文件过大。'));
158
+ try{routes=JSON.parse(fs.readFileSync(file,'utf8'));}catch{throw new Error(t('路由文件不是有效的 JSON。'));}
159
+ }
160
+ const executable=opts.executable?path.resolve(opts.executable):pos[1]==='pi'?findExecutable('pi'):undefined;
161
+ result=request('connect',{application:pos[1],upstream:pos[2],workspace:path.resolve(opts.workspace||os.homedir()),name:opts.name,qr:opts.qr===true,adapter:opts.adapter?path.resolve(opts.adapter):undefined,codex:opts.codex?path.resolve(opts.codex):undefined,codex_home:process.env.CODEX_HOME,search_path:process.env.PATH,executable,profile:opts.profile?path.resolve(opts.profile):process.env.CODING_HARNESS_HOME&&['claude','dsh','qoder'].includes(pos[1])?path.join(process.env.CODING_HARNESS_HOME,pos[1]==='dsh'?'deepseek':pos[1],'profile.json'):undefined,agent_dir:opts['agent-dir']?path.resolve(opts['agent-dir']):process.env.PI_CODING_AGENT_DIR,routes});
162
+ if(result.qr&&!opts.json){require('qrcode-terminal').generate(result.qr,{small:true});console.log(t('使用已登录的 Linkitty 手机端扫码认领。'));}
163
+ break;
164
+ }
165
+ case 'access': {
166
+ if(pos.length!==2||!opts.capabilities||!opts.output)throw new Error(t('用法:linkitty access 实例ID --capabilities api.read,api.write --output 私有文件'));
167
+ const capabilities=opts.capabilities.split(',').map(s=>s.trim()).filter(Boolean);
168
+ const output=path.resolve(opts.output);
169
+ const fd=fs.openSync(output,'wx',0o600);
170
+ try{
171
+ const access=request('open_api',{id:pos[1],client_id:'linkitty-cli',capabilities,origins:[]});
172
+ fs.writeFileSync(fd,JSON.stringify(access,null,2)+'\n');
173
+ result={output,endpoint:access.endpoint,expires_at:access.expires_at};
174
+ }catch(error){fs.closeSync(fd);fs.unlinkSync(output);throw error;}
175
+ fs.closeSync(fd);break;
176
+ }
177
+ case 'status':result=request('linkitty_status');break;
178
+ case 'list':result=request('connections');break;
179
+ case 'logout':result=request('logout');break;
180
+ case 'disconnect':if(!pos[1]||pos.length!==2)throw new Error(t('用法:linkitty disconnect ID'));result=request('disconnect',{id:pos[1]});break;
181
+ case 'delete':if(pos.length!==2||!opts.yes)throw new Error(t('用法:linkitty delete 实例ID --yes;永久删除登记和分享授权,电脑文件保留。'));result=request('service_delete',{id:pos[1],confirm_delete:true});break;
182
+ case 'service':if(!['install','status','uninstall'].includes(pos[1])||pos.length!==2)throw new Error(t('用法:linkitty service install|status|uninstall'));result=native(['service',pos[1]]);break;
183
+ default:throw new Error(t('未知命令:')+command+t('\n运行 linkitty --help 查看用法。'));
184
+ }
185
+ if(opts.json){console.log(JSON.stringify(result));return;}
186
+ if(command==='connect'){
187
+ console.log(result.connected?t('服务已接入,连接由后台持续维护。'):t('已登记,等待认领或运行端确认。'));
188
+ if(result.desktop_restart_required)console.log(t('Codex 启动连接已配置。请在当前任务结束后正常重启一次 Codex;之后直接打开即可,无需共控脚本。'));
189
+ }
190
+ else if(command==='login')console.log(t('已登录。运行 linkitty connect codex 接入,不默认绑定项目。'));
191
+ else if(command==='logout')console.log(t('已退出账号。'));
192
+ else console.log(JSON.stringify(result,null,2));
193
+ }
194
+ module.exports={parse,main};
195
+ if(require.main===module)main(process.argv.slice(2)).catch(error=>{console.error(error.message);process.exitCode=1;});
package/lib/i18n.cjs ADDED
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const os = require('node:os');
5
+ const crypto = require('node:crypto');
6
+ const messages = require('./messages.json');
7
+ const supported = ['zh', 'en', 'ja', 'ru'];
8
+ const root = path.resolve(process.env.LINKITTY_HOME || path.join(os.homedir(), '.linkitty'));
9
+ const preferences = path.join(root, 'config', 'preferences.json');
10
+ let saved = {};
11
+ try { saved = JSON.parse(fs.readFileSync(preferences, 'utf8')); } catch {}
12
+ const requested = process.env.LINKITTY_LANG || saved.language || process.env.LC_ALL || process.env.LC_MESSAGES || process.env.LANG || 'en';
13
+ let language = supported.find(code => String(requested).toLowerCase().startsWith(code)) || 'en';
14
+ function t(key, ...values) {
15
+ if (Array.isArray(key)) key = key.reduce((text, part, index) => text + (index ? '{{' + (index - 1) + '}}' : '') + part, '');
16
+ const value = language === 'zh' ? key : messages[key]?.[language] || messages[key]?.en || key;
17
+ return value.replace(/\{\{(\d+)\}\}/g, (match, index) => index < values.length ? String(values[index] ?? '') : match);
18
+ }
19
+ function setLanguage(value) {
20
+ if (!supported.includes(value)) throw new Error(t('语言必须是 zh、en、ja 或 ru。'));
21
+ const directory = path.dirname(preferences);
22
+ fs.mkdirSync(directory, {recursive: true, mode: 0o700});
23
+ for (const folder of [root, directory]) fs.chmodSync(folder, 0o700);
24
+ let current = {};
25
+ if (fs.existsSync(preferences)) current = JSON.parse(fs.readFileSync(preferences, 'utf8'));
26
+ const temporary = preferences + '.tmp-' + crypto.randomBytes(6).toString('hex');
27
+ fs.writeFileSync(temporary, JSON.stringify({...current, language: value}), {flag: 'wx', mode: 0o600});
28
+ fs.renameSync(temporary, preferences);
29
+ language = value;
30
+ }
31
+ module.exports = {t, setLanguage, getLanguage: () => language, supported};
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+ const {t}=require("./i18n.cjs");
3
+ // npm packages install only pinned-version artifacts without starting business logic or reading model credentials.
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const os = require('node:os');
7
+ const crypto = require('node:crypto');
8
+ const {execFileSync} = require('node:child_process');
9
+ // Git Bash GNU tar interprets drive letters as remote hosts; always use Windows' native archive tool.
10
+ const tarCommand=process.platform==='win32'?path.join(process.env.SystemRoot||'C:\\Windows','System32','tar.exe'):'tar';
11
+ function home() {
12
+ const root=path.resolve(process.env.LINKITTY_HOME || path.join(os.homedir(), '.linkitty'));
13
+ fs.mkdirSync(path.join(root,'config'),{recursive:true,mode:0o700});
14
+ for(const name of ['installation.json','desktop.json']) {
15
+ const old=path.join(root,name), next=path.join(root,'config',name);
16
+ if(fs.existsSync(old)&&!fs.existsSync(next))fs.renameSync(old,next);
17
+ }
18
+ return root;
19
+ }
20
+ function platformTag() {
21
+ let arch=process.arch;
22
+ if(process.platform==='darwin'){
23
+ try{if(execFileSync('/usr/sbin/sysctl',['-n','hw.optional.arm64'],{encoding:'utf8'}).trim()==='1')arch='arm64';}catch{}
24
+ }
25
+ return process.platform+'-'+arch;
26
+ }
27
+ function write(file, value) {
28
+ fs.mkdirSync(path.dirname(file), {recursive:true, mode:0o700});
29
+ const temp = file + '.tmp-' + crypto.randomBytes(6).toString('hex');
30
+ fs.writeFileSync(temp, JSON.stringify(value), {mode:0o600, flag:'wx'});
31
+ fs.renameSync(temp,file);
32
+ }
33
+ function install(packageRoot, name) {
34
+ const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot,'artifacts.json'),'utf8'));
35
+ const platform = platformTag();
36
+ const artifact = manifest[platform];
37
+ if (!artifact) throw new Error(t('此安装包尚未包含 ')+platform+t(' 制品。请使用已发布的平台版本。'));
38
+ if (!/^[a-f0-9]{64}$/.test(artifact.sha256)) throw new Error(t('制品摘要缺失。'));
39
+ const archive = path.resolve(packageRoot,artifact.archive);
40
+ if (!archive.startsWith(packageRoot+path.sep)) throw new Error(t('无效制品路径。'));
41
+ const hash = crypto.createHash('sha256').update(fs.readFileSync(archive)).digest('hex');
42
+ if (hash !== artifact.sha256) throw new Error(t('安装制品校验失败。'));
43
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'),'utf8'));
44
+ const destination = path.join(home(),'packages',name,pkg.version,platform+'-'+hash.slice(0,12));
45
+ if (!fs.existsSync(path.join(destination,'.complete'))) {
46
+ fs.mkdirSync(path.dirname(destination),{recursive:true,mode:0o700});
47
+ const stage=fs.mkdtempSync(destination+'.install-');
48
+ try {
49
+ const entries=execFileSync(tarCommand,['-tzf',archive],{encoding:'utf8',maxBuffer:16*1024*1024}).split('\n').filter(Boolean);
50
+ if (entries.some(p=>p.startsWith('/')||p.includes('\\')||p.split('/').includes('..'))) throw new Error(t('安装包包含不安全路径。'));
51
+ execFileSync(tarCommand,['-xzf',archive,'-C',stage],{stdio:'pipe'});
52
+ fs.writeFileSync(path.join(stage,'.complete'),hash,{mode:0o600});
53
+ try { fs.renameSync(stage,destination); }
54
+ catch(error) { if(!fs.existsSync(path.join(destination,'.complete'))) throw error; fs.rmSync(stage,{recursive:true,force:true}); }
55
+ } catch(error) {fs.rmSync(stage,{recursive:true,force:true});throw error;}
56
+ }
57
+ const pointer=path.join(home(),'config',name==='linkitty'?'installation.json':'desktop.json');
58
+ if (name==='linkitty') {
59
+ const runtime=path.join(destination,'bin',process.platform==='win32'?'linkitty-runtime.exe':'linkitty-runtime');
60
+ if(!fs.existsSync(runtime)) throw new Error(t('原生运行时不在安装包内。'));
61
+ const adapter=path.join(destination,'bin',process.platform==='win32'?'relay-coding.exe':'relay-coding');
62
+ write(pointer,{version:pkg.version,platform,runtime,codex_adapter:fs.existsSync(adapter)?adapter:null});
63
+ } else {
64
+ const relative=artifact.executable;
65
+ const executable=path.resolve(destination,relative);
66
+ if(!executable.startsWith(destination+path.sep)||!fs.existsSync(executable)) throw new Error(t('桌面程序不在安装包内。'));
67
+ write(pointer,{version:pkg.version,platform,executable});
68
+ if(process.platform==='darwin'){
69
+ const applications=process.env.LINKITTY_APPLICATIONS_DIR?path.resolve(process.env.LINKITTY_APPLICATIONS_DIR):path.join(os.homedir(),'Applications');
70
+ fs.mkdirSync(applications,{recursive:true});
71
+ const link=path.join(applications,'Linkitty.app');
72
+ const existing=fs.existsSync(link)||(()=>{try{return fs.lstatSync(link).isSymbolicLink();}catch{return false;}})();
73
+ if(!existing || (fs.lstatSync(link).isSymbolicLink() && fs.readlinkSync(link).startsWith(home()+path.sep))){
74
+ const next=link+'.linkitty-next';
75
+ try{fs.unlinkSync(next);}catch{}
76
+ fs.symlinkSync(executable,next);
77
+ fs.renameSync(next,link);
78
+ }
79
+ }
80
+ }
81
+ return JSON.parse(fs.readFileSync(pointer,'utf8'));
82
+ }
83
+ function ensureInstalled(packageRoot,name) {
84
+ const pkg=JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'),'utf8'));
85
+ const key=name==='linkitty'?'runtime':'executable';
86
+ const pointer=path.join(home(),'config',name==='linkitty'?'installation.json':'desktop.json');
87
+ try {
88
+ const current=JSON.parse(fs.readFileSync(pointer,'utf8'));
89
+ if(current.version===pkg.version&&current.platform===platformTag()&&typeof current[key]==='string'&&path.isAbsolute(current[key])){
90
+ const entry=fs.statSync(current[key]);
91
+ if((name==='linkitty-desktop'&&process.platform==='darwin')?entry.isDirectory():entry.isFile())return current;
92
+ }
93
+ } catch {}
94
+ // npm can skip postinstall on upgrades; repair stale or missing pointers from this package's verified payload.
95
+ return install(packageRoot,name);
96
+ }
97
+ function runtime() {
98
+ const override=process.env.LINKITTY_RUNTIME_BIN;
99
+ if(override) {
100
+ if(!path.isAbsolute(override)||!fs.existsSync(override)) throw new Error(t('LINKITTY_RUNTIME_BIN 必须指向实际原生程序。'));
101
+ return override;
102
+ }
103
+ return ensureInstalled(path.resolve(__dirname,'..'),'linkitty').runtime;
104
+ }
105
+ module.exports={home,write,install,ensureInstalled,runtime,platformTag};
106
+ if(require.main===module) {
107
+ try{install(path.resolve(__dirname,'..'),'linkitty');process.stdout.write(t('Linkitty 已安装。运行 linkitty --help 开始。\n'));}
108
+ catch(error){process.stderr.write(error.message+'\n');process.exitCode=1;}
109
+ }