larkway 0.3.57 → 0.3.59
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 +4 -3
- package/README.zh.md +1 -1
- package/bin/larkway.mjs +51 -0
- package/dist/cli/index.js +77 -0
- package/dist/main.js +254 -4
- package/package.json +3 -3
- package/bin/larkway +0 -25
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
You @ the bot in a Feishu thread. It runs on your machine — reading your real codebase, executing commands, opening MRs — and posts the result back. You define what the agent knows and what it can do. Larkway just carries the messages.
|
|
10
10
|
|
|
11
|
-
**Current release: v0.3.
|
|
11
|
+
**Current release: v0.3.59**
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -61,7 +61,7 @@ Feishu thread
|
|
|
61
61
|
|
|
62
62
|
**What Larkway does NOT do** — the agent decides everything else:
|
|
63
63
|
|
|
64
|
-
- No GitLab
|
|
64
|
+
- No GitHub/GitLab API calls (the agent runs `gh`/`glab` itself)
|
|
65
65
|
- No worktree creation (the agent runs `git worktree add`)
|
|
66
66
|
- No dev server management (the agent starts it)
|
|
67
67
|
- No orchestration logic — the agent reads your `AGENTS.md` / `CLAUDE.md` / skills and plans its own steps
|
|
@@ -194,10 +194,11 @@ platform-fact writeups: [docs/task-handle.md](docs/task-handle.md).
|
|
|
194
194
|
|
|
195
195
|
## Requirements
|
|
196
196
|
|
|
197
|
+
- **macOS or Linux** — native Windows is not supported yet (the runtime depends on POSIX tooling such as `bash` and `pgrep`); on Windows, install and run everything (Node.js, larkway, Claude Code / Codex, `lark-cli`) inside [WSL](https://learn.microsoft.com/windows/wsl/install)
|
|
197
198
|
- **Node.js 20+ LTS**
|
|
198
199
|
- **A Claude Code or Codex subscription** with local CLI installed and logged in
|
|
199
200
|
- **`lark-cli`** — Feishu long-connection client and message utilities: `npm i -g @larksuite/cli`, then `lark-cli auth login` (the agent uses it to read thread history and attachments; `larkway doctor` checks it's present)
|
|
200
|
-
- **`
|
|
201
|
+
- **`git` + `gh`** — for bots that open PRs (GitHub is the default forge; use `glab` instead for GitLab repos; both optional for read-only bots)
|
|
201
202
|
- **An always-on host machine** — the bridge must stay running to receive Feishu events; a laptop that sleeps will miss messages; a small server or desktop works well
|
|
202
203
|
|
|
203
204
|
---
|
package/README.zh.md
CHANGED
package/bin/larkway.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// larkway CLI shim — resolves the package root from this script's own location
|
|
3
|
+
// so it works whether invoked via `bin/larkway`, an absolute path, or a symlink
|
|
4
|
+
// on PATH (e.g. after `npm i -g`).
|
|
5
|
+
//
|
|
6
|
+
// Written in Node (not bash) so npm's cross-platform bin shims work everywhere:
|
|
7
|
+
// a bash shebang made Windows installs fail at startup with
|
|
8
|
+
// `/bin/bash: C:Users...: No such file or directory` before any of our code ran.
|
|
9
|
+
//
|
|
10
|
+
// Startup priority:
|
|
11
|
+
// 1. dist/cli/index.js exists → import it in-process (installed package / post-build)
|
|
12
|
+
// 2. otherwise → `npx tsx src/cli/index.ts` (fresh clone dev mode, no build)
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
|
+
|
|
18
|
+
if (process.platform === "win32") {
|
|
19
|
+
// The bridge runtime depends on POSIX tooling (bash supervisor, pgrep, lsof),
|
|
20
|
+
// so native Windows is not supported yet — fail fast with guidance instead of
|
|
21
|
+
// crashing later with an obscure error.
|
|
22
|
+
console.error(
|
|
23
|
+
[
|
|
24
|
+
"larkway 暂不支持原生 Windows 运行(依赖 bash / pgrep 等 POSIX 工具)。",
|
|
25
|
+
"推荐在 WSL(Windows Subsystem for Linux)中安装使用:",
|
|
26
|
+
" 1. 安装 WSL: https://learn.microsoft.com/windows/wsl/install",
|
|
27
|
+
" 2. 在 WSL 终端内安装 Node.js 20+,然后 `npm i -g larkway`",
|
|
28
|
+
" 3. Claude Code / Codex 也需安装在 WSL 内",
|
|
29
|
+
"",
|
|
30
|
+
"larkway does not yet support native Windows (it depends on POSIX tooling",
|
|
31
|
+
"such as bash and pgrep). Please install and run it inside WSL instead.",
|
|
32
|
+
].join("\n"),
|
|
33
|
+
);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
38
|
+
const repoRoot = path.resolve(path.dirname(selfPath), "..");
|
|
39
|
+
const distEntry = path.join(repoRoot, "dist", "cli", "index.js");
|
|
40
|
+
|
|
41
|
+
if (existsSync(distEntry)) {
|
|
42
|
+
await import(pathToFileURL(distEntry).href);
|
|
43
|
+
} else {
|
|
44
|
+
// Dev fallback: run TypeScript source directly via tsx.
|
|
45
|
+
const result = spawnSync(
|
|
46
|
+
"npx",
|
|
47
|
+
["tsx", path.join(repoRoot, "src", "cli", "index.ts"), ...process.argv.slice(2)],
|
|
48
|
+
{ stdio: "inherit" },
|
|
49
|
+
);
|
|
50
|
+
process.exit(result.status ?? 1);
|
|
51
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -109971,6 +109971,70 @@ var init_client = __esm({
|
|
|
109971
109971
|
wrapErr(label, err2);
|
|
109972
109972
|
}
|
|
109973
109973
|
}
|
|
109974
|
+
/**
|
|
109975
|
+
* task_handle v5 (BL-48, 声明式建卡): create a task under this bot's app
|
|
109976
|
+
* identity. `due.timestamp` is ms-epoch string per task v2; `members` lets
|
|
109977
|
+
* the create carry the originator as follower in one call; `tasklists`
|
|
109978
|
+
* attaches the task to the bot's configured list when present. Response
|
|
109979
|
+
* shape mirrors createTasklist's envelope (`data.task.guid`).
|
|
109980
|
+
*/
|
|
109981
|
+
async createTask(input) {
|
|
109982
|
+
const label = `createTask(${input.summary.slice(0, 30)})`;
|
|
109983
|
+
let resp;
|
|
109984
|
+
try {
|
|
109985
|
+
resp = await this.#request(
|
|
109986
|
+
{
|
|
109987
|
+
method: "POST",
|
|
109988
|
+
url: `${TASK_V2_BASE}/tasks`,
|
|
109989
|
+
params: { user_id_type: "open_id" },
|
|
109990
|
+
data: {
|
|
109991
|
+
summary: input.summary,
|
|
109992
|
+
...input.description ? { description: input.description } : {},
|
|
109993
|
+
...input.due ? { due: input.due } : {},
|
|
109994
|
+
...input.members && input.members.length > 0 ? { members: input.members } : {},
|
|
109995
|
+
...input.tasklists && input.tasklists.length > 0 ? { tasklists: input.tasklists } : {}
|
|
109996
|
+
}
|
|
109997
|
+
},
|
|
109998
|
+
label
|
|
109999
|
+
);
|
|
110000
|
+
} catch (err2) {
|
|
110001
|
+
wrapErr(label, err2);
|
|
110002
|
+
}
|
|
110003
|
+
const guid = resp.data?.task?.["guid"];
|
|
110004
|
+
if (typeof guid !== "string" || guid.length === 0) {
|
|
110005
|
+
throw new TaskApiError(`[tasklist.client] ${label} returned no guid`, {});
|
|
110006
|
+
}
|
|
110007
|
+
return { guid };
|
|
110008
|
+
}
|
|
110009
|
+
/**
|
|
110010
|
+
* task_handle v5: add members (e.g. a late follower) to an existing TASK
|
|
110011
|
+
* (`POST /tasks/:guid/add_members` — task-level counterpart of
|
|
110012
|
+
* addTasklistMembers). Best-effort semantics at the caller, same as every
|
|
110013
|
+
* other write here.
|
|
110014
|
+
*/
|
|
110015
|
+
async addTaskMembers(taskGuid, members) {
|
|
110016
|
+
const label = `addTaskMembers(${taskGuid})`;
|
|
110017
|
+
try {
|
|
110018
|
+
await this.#request(
|
|
110019
|
+
{
|
|
110020
|
+
method: "POST",
|
|
110021
|
+
url: `${TASK_V2_BASE}/tasks/${encodeURIComponent(taskGuid)}/add_members`,
|
|
110022
|
+
params: { user_id_type: "open_id" },
|
|
110023
|
+
data: { members }
|
|
110024
|
+
},
|
|
110025
|
+
label
|
|
110026
|
+
);
|
|
110027
|
+
} catch (err2) {
|
|
110028
|
+
wrapErr(label, err2);
|
|
110029
|
+
}
|
|
110030
|
+
}
|
|
110031
|
+
/**
|
|
110032
|
+
* task_handle v5: reschedule (or set) the task due. ms-epoch string per
|
|
110033
|
+
* task v2 `due.timestamp`; same patch semantics as complete()/reopen().
|
|
110034
|
+
*/
|
|
110035
|
+
async patchDue(taskGuid, due) {
|
|
110036
|
+
await this.#patchTask(taskGuid, { due }, ["due"]);
|
|
110037
|
+
}
|
|
109974
110038
|
};
|
|
109975
110039
|
}
|
|
109976
110040
|
});
|
|
@@ -127416,6 +127480,9 @@ function repoLooksGitLab(urlOrSlug) {
|
|
|
127416
127480
|
function botUsesGitLab(bot) {
|
|
127417
127481
|
return bot.repos.some((repo) => repoLooksGitLab(repo.url ?? repo.slug));
|
|
127418
127482
|
}
|
|
127483
|
+
function botUsesGitHub(bot) {
|
|
127484
|
+
return bot.repos.some((repo) => !repoLooksGitLab(repo.url ?? repo.slug));
|
|
127485
|
+
}
|
|
127419
127486
|
function addCliRequirement(requirements, input) {
|
|
127420
127487
|
const existing = requirements.get(`cli:${input.command}`);
|
|
127421
127488
|
if (existing) {
|
|
@@ -127499,6 +127566,16 @@ function runtimeRequirementsForBots(bots) {
|
|
|
127499
127566
|
});
|
|
127500
127567
|
}
|
|
127501
127568
|
}
|
|
127569
|
+
if (botUsesGitHub(bot)) {
|
|
127570
|
+
addCliRequirement(requirements, {
|
|
127571
|
+
command: "gh",
|
|
127572
|
+
label: "GitHub CLI",
|
|
127573
|
+
severity: "optional",
|
|
127574
|
+
botIds: [bot.id],
|
|
127575
|
+
reason: "Only needed when an agent uses GitHub-specific actions such as PRs, issues, or releases.",
|
|
127576
|
+
installHint: "Install gh only for bots that need GitHub-specific workflow commands."
|
|
127577
|
+
});
|
|
127578
|
+
}
|
|
127502
127579
|
if (botUsesGitLab(bot)) {
|
|
127503
127580
|
addCliRequirement(requirements, {
|
|
127504
127581
|
command: "glab",
|
package/dist/main.js
CHANGED
|
@@ -114958,6 +114958,13 @@ function renderStateContract(stateFilePath) {
|
|
|
114958
114958
|
"- \u9700\u8981\u8986\u76D6\u5361\u7247\u6B63\u6587:status=ready + last_message(\u4E0D\u5199\u65F6\u6B63\u6587=\u7B54\u6848\u901A\u9053\u5185\u5BB9);dev_url/mr_url \u7B49\u4E1A\u52A1\u5B57\u6BB5\u53EF\u81EA\u7531\u5199\u5165,\u4F46 bridge \u4E0D\u611F\u77E5\u5176\u542B\u4E49,\u8981\u8BA9\u7528\u6237\u770B\u5230\u5C31\u5199\u8FDB\u6B63\u6587\u3002",
|
|
114959
114959
|
"- \u9700\u8981\u5728\u7EC8\u6001\u5361\u4E0A @ \u4EBA:response_surface \u5199 `{post:{mentions:[{user_id}]}}`;\u8FD9\u53EA\u662F\u89C6\u89C9\u63D0\u793A,\u8981 peer bot \u6D88\u8D39\u6B63\u6587\u5FC5\u987B\u8D70 handoffs \u6216\u53E6\u53D1\u771F\u5B9E post(\u5361\u7247\u5BF9 agent \u4E0D\u53EF\u8BFB)\u3002",
|
|
114960
114960
|
"- \u4EA4\u63A5\u7ED9 peer bot:handoffs(\u6700\u591A 3 \u6761 `{to, text}`,to \u5199 peer-bots \u540D\u518C\u91CC\u7684\u540D\u5B57)\u3002bridge \u66FF\u4F60\u53D1\u4E00\u6761\u5E26\u771F\u5B9E at \u6807\u7B7E\u7684 post \u7559\u75D5,\u540C\u6865\u8FDB\u7A0B\u5185\u7684 peer \u8FD8\u4F1A\u88AB**\u672C\u5730\u76F4\u9012**\u7ACB\u5373\u5524\u9192\u2014\u2014\u6BD4\u81EA\u5DF1\u7528 lark-cli \u53D1 post \u66F4\u5FEB\u66F4\u53EF\u9760\u3002text \u5FC5\u987B\u81EA\u5305\u542B:\u5BF9\u65B9\u53EA\u770B\u5F97\u5230\u8FD9\u6761\u6587\u672C,\u80CC\u666F\u3001\u8981\u505A\u4EC0\u4E48\u3001\u671F\u671B\u4EA7\u51FA\u90FD\u5199\u6E05\u695A\u3002",
|
|
114961
|
+
"- **\u4EFB\u52A1\u5361(\u4E94\u4FE1\u53F7\u59D4\u6258\u5951\u7EA6,\u503C\u5F97\u8FFD\u8E2A\u7684\u6D3B\u5FC5\u7528)**:\u5224\u65AD\u6807\u51C6\u2014\u2014\u4E09\u4E2A\u6708\u540E\u590D\u76D8\u4F1A\u627E\u5B83\u3001\u6216\u6267\u884C\u8DE8\u8D8A\u672C\u8F6E\u5BF9\u8BDD(\u8981\u8DD1\u5F88\u4E45/\u7B49\u5916\u90E8/\u5206\u9636\u6BB5)\u2192 \u5199 task_handle\u3002\u4E00\u95EE\u4E00\u7B54\u804A\u5B8C\u5C31\u5B8C\u7684**\u4E0D\u5EFA\u5361**\u3002",
|
|
114962
|
+
" - \u63A5\u4E86:`task_handle: {create: {summary, due?}}`\u2014\u2014bridge \u81EA\u52A8\u5EFA\u5361\u3001\u628A\u8BDD\u9898\u56DE\u94FE\u5199\u8FDB\u4EFB\u52A1\u63CF\u8FF0\u3001\u628A\u53D1\u8D77\u4EBA\u52A0\u8FDB\u5173\u6CE8\u5217\u8868,\u5E76\u7ED1\u5B9A\u672C\u8BDD\u9898\u3002\u5DF2\u6709\u4EFB\u52A1\u5219\u7528 `{guid}` \u8BA4\u9886\u3002",
|
|
114963
|
+
' - \u8FDB\u5C55:`{note: "\u4E00\u53E5\u8BDD\u91CC\u7A0B\u7891"}` \u53EA\u5728\u5B9E\u8D28\u8FDB\u5C55\u65F6\u5199(\u9636\u6BB5\u7ED3\u8BBA/\u5173\u952E\u8F6C\u6298),bridge \u843D\u5230\u4EFB\u52A1\u4FA7;\u4E0D\u8981\u6BCF\u8F6E\u90FD\u5199\u3002',
|
|
114964
|
+
' - \u6539\u671F:`{due: "<ISO \u6216 YYYY-MM-DD>", due_reason: "\u4E00\u53E5\u539F\u56E0"}`\u2014\u2014\u5EF6\u671F\u5FC5\u987B\u5E26\u539F\u56E0,bridge \u4F1A\u6539\u5361\u4E0A\u622A\u6B62\u5E76\u8BC4\u8BBA\u8BF4\u660E\u3002',
|
|
114965
|
+
" - \u5B8C\u6210:`{done: true, note: ...}` \u4EA4\u4ED8\u8F6E\u624D\u5199;\u5931\u8D25\u8BA9\u4EFB\u52A1\u4FDD\u6301 open\u3002",
|
|
114966
|
+
' - \u963B\u585E:`{blocked: "\u5361\u5728\u54EA\u3001\u9700\u8981\u4EBA\u505A\u4EC0\u4E48"}`\u2014\u2014bridge \u843D\u4EFB\u52A1\u8BC4\u8BBA,\u5173\u6CE8\u4EBA\u4F1A\u6536\u5230\u901A\u77E5;\u540C\u65F6\u5728\u6B63\u6587\u7528 choices \u628A\u51B3\u5B9A\u6536\u655B\u6210\u9009\u9879\u3002',
|
|
114967
|
+
" - \u4EA4\u4ED8\u53CC\u6307\u9488:\u7ED3\u8BBA\u62A5\u544A\u5148\u7528 lark-cli \u5BFC\u6210**\u98DE\u4E66\u6587\u6863**,\u56DE\u590D\u548C\u4EFB\u52A1\u8BC4\u8BBA\u91CC doc \u94FE\u63A5\u5728\u524D\u3001\u672C\u5730\u7EDD\u5BF9\u8DEF\u5F84\u5728\u540E(\u672C\u5730\u6587\u4EF6\u662F\u53D6\u8BC1\u771F\u76F8,doc \u662F\u7ED9\u4EBA\u770B\u7684\u5F62\u6001)\u3002",
|
|
114961
114968
|
"",
|
|
114962
114969
|
"\u8FB9\u754C\u4E0E\u8D23\u4EFB:",
|
|
114963
114970
|
"- **\u7EDD\u4E0D\u81EA\u5DF1 PATCH/PUT bridge \u7BA1\u7406\u7684\u5361\u7247/post** \u2014\u2014 \u7F51\u7EDC\u66F4\u65B0\u662F bridge \u7684\u6D3B,\u81EA\u5DF1\u6539\u4F1A\u548C\u5361\u7247 finalize\u3001\u6309\u94AE\u56DE\u4F20\u3001\u5D29\u6E83\u6062\u590D\u51B2\u7A81\u3002",
|
|
@@ -114970,7 +114977,7 @@ function renderContractAnchor(stateFilePath) {
|
|
|
114970
114977
|
const stateTarget = stateFilePath ? `\`${stateFilePath}\`` : "\u5DE5\u4F5C\u76EE\u5F55\u91CC\u7684 `.larkway/state.json`";
|
|
114971
114978
|
return [
|
|
114972
114979
|
"<contract-anchor>",
|
|
114973
|
-
`\u5951\u7EA6\u540C\u672C\u8BDD\u9898\u9996\u8F6E\u6CE8\u5165,\u672A\u53D8\u5316:\u7ED9\u7528\u6237\u770B\u7684\u7B54\u6848\u6B63\u6587\u5305\u5728\u72EC\u7ACB\u884C marker \`${ANSWER_BEGIN_MARKER}\` / \`${ANSWER_END_MARKER}\` \u4E4B\u95F4\u8F93\u51FA;\u7ED3\u8BBA\u4E00\u6210\u5F62\u5C31\u5148\u6D41\u51FA\u7B54\u6848\u3002\u7EAF\u6587\u5B57\u56DE\u7B54\u4E0D\u7528\u5199 state.json;\u5931\u8D25/\u7B49\u8865\u5145/choices/\u56FE\u6587\u6DF7\u6392/handoffs \u4EA4\u63A5\u65F6\u624D\u5199 ${stateTarget}(\u539F\u5B50\u5199 + \u5237\u65B0 updated_at)\u3002\u505A\u5B8C\u5E72\u51C0\u9000\u51FA\u8FDB\u7A0B\u3002`,
|
|
114980
|
+
`\u5951\u7EA6\u540C\u672C\u8BDD\u9898\u9996\u8F6E\u6CE8\u5165,\u672A\u53D8\u5316:\u7ED9\u7528\u6237\u770B\u7684\u7B54\u6848\u6B63\u6587\u5305\u5728\u72EC\u7ACB\u884C marker \`${ANSWER_BEGIN_MARKER}\` / \`${ANSWER_END_MARKER}\` \u4E4B\u95F4\u8F93\u51FA;\u7ED3\u8BBA\u4E00\u6210\u5F62\u5C31\u5148\u6D41\u51FA\u7B54\u6848\u3002\u7EAF\u6587\u5B57\u56DE\u7B54\u4E0D\u7528\u5199 state.json;\u5931\u8D25/\u7B49\u8865\u5145/choices/\u56FE\u6587\u6DF7\u6392/handoffs \u4EA4\u63A5/task_handle \u4EFB\u52A1\u4FE1\u53F7(\u5EFA\u5361/\u91CC\u7A0B\u7891/\u6539\u671F/\u963B\u585E/\u4EA4\u4ED8)\u65F6\u624D\u5199 ${stateTarget}(\u539F\u5B50\u5199 + \u5237\u65B0 updated_at)\u3002\u505A\u5B8C\u5E72\u51C0\u9000\u51FA\u8FDB\u7A0B\u3002`,
|
|
114974
114981
|
"</contract-anchor>"
|
|
114975
114982
|
];
|
|
114976
114983
|
}
|
|
@@ -115424,7 +115431,7 @@ async function renderPrompt(input) {
|
|
|
115424
115431
|
` lark-cli docs +get <doc-url>${profileFlag}`,
|
|
115425
115432
|
"- \u53D6\u672C\u6761\u6D88\u606F\u7684\u9644\u4EF6/\u5185\u8054\u56FE(post \u5185\u8054\u56FE\u4E0D\u5728\u4E0A\u9762 attachments/images \u91CC)\u3001\u62C9\u8BDD\u9898\u5386\u53F2:",
|
|
115426
115433
|
attachmentHelpLine,
|
|
115427
|
-
"-
|
|
115434
|
+
"- gh / glab / git API",
|
|
115428
115435
|
"- pnpm / npm",
|
|
115429
115436
|
"",
|
|
115430
115437
|
chatHistoryFirst ? "**\u91CD\u8981**:\u8FD9\u662F\u5355\u804A/\u7C98\u8FDE session \u573A\u666F,\u4E0A\u4E0B\u6587\u5386\u53F2\u7528\u4E0A\u9762\u7684 chat-messages-list \u62C9\u53D6;\u4E0D\u8981\u628A `thread_id` \u5F53\u4F5C\u53EF\u62C9\u53D6\u7684\u6D88\u606F id \u4F7F\u7528\u3002" : "**\u91CD\u8981**:`thread_id` \u5C31\u662F\u8BDD\u9898\u9996\u697C\u7684 message_id\u3002\u5982\u679C\u5F53\u524D\u6D88\u606F attachments/feishu_doc_links \u4E3A\u7A7A,\u8BF4\u660E\u8FD0\u8425\u628A\u7D20\u6750\u653E\u5728\u9996\u697C,**\u5148\u62C9\u9996\u697C\u770B\u8FD0\u8425\u539F\u59CB\u9700\u6C42**,\u518D\u51B3\u5B9A\u4E0B\u4E00\u6B65\u3002",
|
|
@@ -116921,7 +116928,26 @@ var StateFileSchema = external_exports.object({
|
|
|
116921
116928
|
* 不要粘贴完整回复;bridge 侧仍有 sanitizeSummary 的机械截断兜底(见
|
|
116922
116929
|
* src/tasklist/writeback.ts),但那只防"过长",不防"文不对题"。
|
|
116923
116930
|
*/
|
|
116924
|
-
|
|
116931
|
+
/**
|
|
116932
|
+
* v5 (BL-48, 五信号委托契约): `guid` becomes optional because `create` is the
|
|
116933
|
+
* new way a task comes into existence — agent 自判「这个活值得追踪」时声明
|
|
116934
|
+
* create,bridge 机械建卡(话题回链 + 发起人进关注列表)并自动认领。其余
|
|
116935
|
+
* 新字段同样是声明式信号:`due`(+`due_reason`)= 改期/设期,`blocked` =
|
|
116936
|
+
* 阻塞需要人(bridge 落任务评论,关注人收到推送)。全部按需、互不依赖;
|
|
116937
|
+
* bridge 不判断该不该建卡/该不该改期 —— 判断永远在 agent 侧(薄通道)。
|
|
116938
|
+
*/
|
|
116939
|
+
task_handle: external_exports.object({
|
|
116940
|
+
guid: external_exports.string().min(1).optional(),
|
|
116941
|
+
create: external_exports.object({
|
|
116942
|
+
summary: external_exports.string().trim().min(1).max(500),
|
|
116943
|
+
due: external_exports.string().trim().min(1).optional()
|
|
116944
|
+
}).optional(),
|
|
116945
|
+
due: external_exports.string().trim().min(1).optional(),
|
|
116946
|
+
due_reason: external_exports.string().optional(),
|
|
116947
|
+
blocked: external_exports.string().optional(),
|
|
116948
|
+
done: external_exports.boolean().optional(),
|
|
116949
|
+
note: external_exports.string().optional()
|
|
116950
|
+
}).optional(),
|
|
116925
116951
|
/**
|
|
116926
116952
|
* Freshness signal for the handler's stale-guard (it compares this against a
|
|
116927
116953
|
* pre-run snapshot to decide whether the bot rewrote state THIS turn).
|
|
@@ -118766,6 +118792,7 @@ stderr: ${stderr}`));
|
|
|
118766
118792
|
var CORE_ALLOW_RULES = [
|
|
118767
118793
|
"Bash(lark-cli *)",
|
|
118768
118794
|
"Bash(git *)",
|
|
118795
|
+
"Bash(gh *)",
|
|
118769
118796
|
"Bash(glab *)",
|
|
118770
118797
|
"Bash(lsof *)",
|
|
118771
118798
|
"Bash(curl *)",
|
|
@@ -120150,7 +120177,45 @@ var BridgeHandler = class {
|
|
|
120150
120177
|
reason: rawReportedState === null ? "\u672C\u8F6E\u7ED3\u675F\u65F6\u672A\u8BFB\u53D6\u5230 state.json\u3002" : "\u672C\u8F6E state.json \u6CA1\u6709 fresh updated_at\uFF0C\u5DF2\u5FFD\u7565\u65E7\u72B6\u6001\u3002"
|
|
120151
120178
|
});
|
|
120152
120179
|
}
|
|
120153
|
-
const
|
|
120180
|
+
const declaredTaskHandle = reportedState?.task_handle;
|
|
120181
|
+
let bridgeCreatedTaskGuid;
|
|
120182
|
+
if (declaredTaskHandle && (declaredTaskHandle.create || declaredTaskHandle.due || declaredTaskHandle.blocked) && this.deps.taskHandleDeclare && botId) {
|
|
120183
|
+
try {
|
|
120184
|
+
let topicLink;
|
|
120185
|
+
if (declaredTaskHandle.create) {
|
|
120186
|
+
const direct = realTopicThreadId(parsed.raw.thread_id);
|
|
120187
|
+
if (direct) {
|
|
120188
|
+
topicLink = buildTopicDeepLink(parsed.chatId, direct);
|
|
120189
|
+
} else if (this.deps.messageLookup) {
|
|
120190
|
+
const refreshed = await this.deps.messageLookup.get(replyAnchorId, { refresh: true }).catch(() => void 0);
|
|
120191
|
+
const refreshedThreadId = realTopicThreadId(refreshed?.threadId);
|
|
120192
|
+
if (refreshedThreadId) {
|
|
120193
|
+
topicLink = buildTopicDeepLink(parsed.chatId, refreshedThreadId);
|
|
120194
|
+
}
|
|
120195
|
+
}
|
|
120196
|
+
}
|
|
120197
|
+
const result2 = await this.deps.taskHandleDeclare({
|
|
120198
|
+
botId,
|
|
120199
|
+
threadId,
|
|
120200
|
+
chatId: parsed.chatId,
|
|
120201
|
+
senderOpenId: parsed.senderOpenId || void 0,
|
|
120202
|
+
create: declaredTaskHandle.create,
|
|
120203
|
+
declaredGuid: declaredTaskHandle.guid,
|
|
120204
|
+
due: declaredTaskHandle.due,
|
|
120205
|
+
dueReason: declaredTaskHandle.due_reason,
|
|
120206
|
+
blocked: declaredTaskHandle.blocked,
|
|
120207
|
+
topicLink,
|
|
120208
|
+
chatLink: `https://applink.feishu.cn/client/chat/open?openChatId=${parsed.chatId}`
|
|
120209
|
+
});
|
|
120210
|
+
bridgeCreatedTaskGuid = result2?.createdGuid;
|
|
120211
|
+
for (const line of result2?.outcomes ?? []) {
|
|
120212
|
+
await recordEvent({ status: "running", appendPath: "\u4EFB\u52A1\u4FE1\u53F7", reason: line });
|
|
120213
|
+
}
|
|
120214
|
+
} catch (err) {
|
|
120215
|
+
console.warn("[bridge.handler] taskHandleDeclare hook failed (continuing):", err);
|
|
120216
|
+
}
|
|
120217
|
+
}
|
|
120218
|
+
const claimedTaskGuid = bridgeCreatedTaskGuid ?? reportedState?.task_handle?.guid;
|
|
120154
120219
|
if (claimedTaskGuid && this.deps.taskHandleClaim && botId) {
|
|
120155
120220
|
try {
|
|
120156
120221
|
await this.deps.taskHandleClaim({
|
|
@@ -127630,6 +127695,9 @@ function repoLooksGitLab(urlOrSlug) {
|
|
|
127630
127695
|
function botUsesGitLab(bot) {
|
|
127631
127696
|
return bot.repos.some((repo) => repoLooksGitLab(repo.url ?? repo.slug));
|
|
127632
127697
|
}
|
|
127698
|
+
function botUsesGitHub(bot) {
|
|
127699
|
+
return bot.repos.some((repo) => !repoLooksGitLab(repo.url ?? repo.slug));
|
|
127700
|
+
}
|
|
127633
127701
|
function addCliRequirement(requirements, input) {
|
|
127634
127702
|
const existing = requirements.get(`cli:${input.command}`);
|
|
127635
127703
|
if (existing) {
|
|
@@ -127713,6 +127781,16 @@ function runtimeRequirementsForBots(bots) {
|
|
|
127713
127781
|
});
|
|
127714
127782
|
}
|
|
127715
127783
|
}
|
|
127784
|
+
if (botUsesGitHub(bot)) {
|
|
127785
|
+
addCliRequirement(requirements, {
|
|
127786
|
+
command: "gh",
|
|
127787
|
+
label: "GitHub CLI",
|
|
127788
|
+
severity: "optional",
|
|
127789
|
+
botIds: [bot.id],
|
|
127790
|
+
reason: "Only needed when an agent uses GitHub-specific actions such as PRs, issues, or releases.",
|
|
127791
|
+
installHint: "Install gh only for bots that need GitHub-specific workflow commands."
|
|
127792
|
+
});
|
|
127793
|
+
}
|
|
127716
127794
|
if (botUsesGitLab(bot)) {
|
|
127717
127795
|
addCliRequirement(requirements, {
|
|
127718
127796
|
command: "glab",
|
|
@@ -128362,6 +128440,70 @@ var TaskListClient = class {
|
|
|
128362
128440
|
wrapErr(label, err);
|
|
128363
128441
|
}
|
|
128364
128442
|
}
|
|
128443
|
+
/**
|
|
128444
|
+
* task_handle v5 (BL-48, 声明式建卡): create a task under this bot's app
|
|
128445
|
+
* identity. `due.timestamp` is ms-epoch string per task v2; `members` lets
|
|
128446
|
+
* the create carry the originator as follower in one call; `tasklists`
|
|
128447
|
+
* attaches the task to the bot's configured list when present. Response
|
|
128448
|
+
* shape mirrors createTasklist's envelope (`data.task.guid`).
|
|
128449
|
+
*/
|
|
128450
|
+
async createTask(input) {
|
|
128451
|
+
const label = `createTask(${input.summary.slice(0, 30)})`;
|
|
128452
|
+
let resp;
|
|
128453
|
+
try {
|
|
128454
|
+
resp = await this.#request(
|
|
128455
|
+
{
|
|
128456
|
+
method: "POST",
|
|
128457
|
+
url: `${TASK_V2_BASE}/tasks`,
|
|
128458
|
+
params: { user_id_type: "open_id" },
|
|
128459
|
+
data: {
|
|
128460
|
+
summary: input.summary,
|
|
128461
|
+
...input.description ? { description: input.description } : {},
|
|
128462
|
+
...input.due ? { due: input.due } : {},
|
|
128463
|
+
...input.members && input.members.length > 0 ? { members: input.members } : {},
|
|
128464
|
+
...input.tasklists && input.tasklists.length > 0 ? { tasklists: input.tasklists } : {}
|
|
128465
|
+
}
|
|
128466
|
+
},
|
|
128467
|
+
label
|
|
128468
|
+
);
|
|
128469
|
+
} catch (err) {
|
|
128470
|
+
wrapErr(label, err);
|
|
128471
|
+
}
|
|
128472
|
+
const guid = resp.data?.task?.["guid"];
|
|
128473
|
+
if (typeof guid !== "string" || guid.length === 0) {
|
|
128474
|
+
throw new TaskApiError(`[tasklist.client] ${label} returned no guid`, {});
|
|
128475
|
+
}
|
|
128476
|
+
return { guid };
|
|
128477
|
+
}
|
|
128478
|
+
/**
|
|
128479
|
+
* task_handle v5: add members (e.g. a late follower) to an existing TASK
|
|
128480
|
+
* (`POST /tasks/:guid/add_members` — task-level counterpart of
|
|
128481
|
+
* addTasklistMembers). Best-effort semantics at the caller, same as every
|
|
128482
|
+
* other write here.
|
|
128483
|
+
*/
|
|
128484
|
+
async addTaskMembers(taskGuid, members) {
|
|
128485
|
+
const label = `addTaskMembers(${taskGuid})`;
|
|
128486
|
+
try {
|
|
128487
|
+
await this.#request(
|
|
128488
|
+
{
|
|
128489
|
+
method: "POST",
|
|
128490
|
+
url: `${TASK_V2_BASE}/tasks/${encodeURIComponent(taskGuid)}/add_members`,
|
|
128491
|
+
params: { user_id_type: "open_id" },
|
|
128492
|
+
data: { members }
|
|
128493
|
+
},
|
|
128494
|
+
label
|
|
128495
|
+
);
|
|
128496
|
+
} catch (err) {
|
|
128497
|
+
wrapErr(label, err);
|
|
128498
|
+
}
|
|
128499
|
+
}
|
|
128500
|
+
/**
|
|
128501
|
+
* task_handle v5: reschedule (or set) the task due. ms-epoch string per
|
|
128502
|
+
* task v2 `due.timestamp`; same patch semantics as complete()/reopen().
|
|
128503
|
+
*/
|
|
128504
|
+
async patchDue(taskGuid, due) {
|
|
128505
|
+
await this.#patchTask(taskGuid, { due }, ["due"]);
|
|
128506
|
+
}
|
|
128365
128507
|
};
|
|
128366
128508
|
function isNotFoundLikeRaw(err) {
|
|
128367
128509
|
const rec = asRecord2(err);
|
|
@@ -128583,6 +128725,106 @@ async function applyAutoBindConfirmation(taskGuid, client) {
|
|
|
128583
128725
|
}
|
|
128584
128726
|
}
|
|
128585
128727
|
|
|
128728
|
+
// src/tasklist/declare.ts
|
|
128729
|
+
function parseDueInput(raw) {
|
|
128730
|
+
const s = raw.trim();
|
|
128731
|
+
if (/^\d{13}$/.test(s)) return { timestamp: s };
|
|
128732
|
+
if (/^\d{10}$/.test(s)) return { timestamp: `${Number(s) * 1e3}` };
|
|
128733
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
|
|
128734
|
+
const ms2 = Date.parse(`${s}T00:00:00`);
|
|
128735
|
+
if (Number.isNaN(ms2)) return null;
|
|
128736
|
+
return { timestamp: `${ms2}`, is_all_day: true };
|
|
128737
|
+
}
|
|
128738
|
+
const ms = Date.parse(s);
|
|
128739
|
+
if (Number.isNaN(ms)) return null;
|
|
128740
|
+
return { timestamp: `${ms}` };
|
|
128741
|
+
}
|
|
128742
|
+
function formatDueForComment(due) {
|
|
128743
|
+
const d = new Date(Number(due.timestamp));
|
|
128744
|
+
if (Number.isNaN(d.getTime())) return due.timestamp;
|
|
128745
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
128746
|
+
const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
128747
|
+
return due.is_all_day ? date : `${date} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
128748
|
+
}
|
|
128749
|
+
function renderCreateDescription(patch, botName) {
|
|
128750
|
+
const lines = [];
|
|
128751
|
+
if (patch.topicLink) {
|
|
128752
|
+
lines.push(`\u8BDD\u9898\uFF1A${patch.topicLink}`);
|
|
128753
|
+
} else if (patch.chatLink) {
|
|
128754
|
+
lines.push(`\u8BDD\u9898\u6DF1\u94FE\u4E0D\u53EF\u7528\uFF08\u975E\u8BDD\u9898\u6D88\u606F\uFF09\uFF0C\u6240\u5728\u7FA4\uFF1A${patch.chatLink}`);
|
|
128755
|
+
} else {
|
|
128756
|
+
lines.push("\u8BDD\u9898\u6DF1\u94FE\u4E0D\u53EF\u7528\uFF08\u672A\u89E3\u6790\u5230\u8BDD\u9898/\u7FA4\u94FE\u63A5\uFF09");
|
|
128757
|
+
}
|
|
128758
|
+
lines.push(`\u7531 ${botName ?? patch.botId} \u521B\u5EFA \xB7 ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
128759
|
+
return lines.join("\n");
|
|
128760
|
+
}
|
|
128761
|
+
async function applyTaskHandleDeclarations(patch, deps) {
|
|
128762
|
+
const outcomes = [];
|
|
128763
|
+
let createdGuid;
|
|
128764
|
+
if (patch.create) {
|
|
128765
|
+
const existing = deps.store.get(patch.threadId);
|
|
128766
|
+
if (existing) {
|
|
128767
|
+
outcomes.push(`create \u8DF3\u8FC7\uFF1A\u8BDD\u9898\u5DF2\u8BA4\u9886\u4EFB\u52A1 ${existing.taskGuid}`);
|
|
128768
|
+
} else {
|
|
128769
|
+
try {
|
|
128770
|
+
const due = patch.create.due ? parseDueInput(patch.create.due) : null;
|
|
128771
|
+
if (patch.create.due && !due) {
|
|
128772
|
+
outcomes.push(`create.due "${patch.create.due}" \u65E0\u6CD5\u89E3\u6790\uFF0C\u4EFB\u52A1\u5C06\u65E0\u622A\u6B62\u65F6\u95F4`);
|
|
128773
|
+
}
|
|
128774
|
+
const members = patch.senderOpenId ? [{ id: patch.senderOpenId, type: "user", role: "follower" }] : [];
|
|
128775
|
+
const { guid } = await deps.client.createTask({
|
|
128776
|
+
summary: patch.create.summary,
|
|
128777
|
+
description: renderCreateDescription(patch, deps.botName),
|
|
128778
|
+
...due ? { due } : {},
|
|
128779
|
+
...members.length > 0 ? { members } : {},
|
|
128780
|
+
...deps.tasklistGuid ? { tasklists: [{ tasklist_guid: deps.tasklistGuid }] } : {}
|
|
128781
|
+
});
|
|
128782
|
+
createdGuid = guid;
|
|
128783
|
+
outcomes.push(
|
|
128784
|
+
`\u5DF2\u5EFA\u4EFB\u52A1 ${guid}\uFF08${patch.create.summary.slice(0, 40)}${due ? ` \xB7 \u622A\u6B62 ${formatDueForComment(due)}` : ""}${patch.senderOpenId ? " \xB7 \u53D1\u8D77\u4EBA\u5DF2\u52A0\u5173\u6CE8" : ""}${patch.topicLink ? "" : "\uFF0C\u8BDD\u9898\u6DF1\u94FE\u7F3A\u5931\u5DF2\u964D\u7EA7\u4E3A\u7FA4\u94FE\u63A5"}\uFF09`
|
|
128785
|
+
);
|
|
128786
|
+
} catch (err) {
|
|
128787
|
+
outcomes.push(`create \u5931\u8D25\uFF08\u672C\u8F6E\u8DF3\u8FC7\uFF0C\u4E0D\u5F71\u54CD\u4EA4\u4ED8\uFF09\uFF1A${String(err.message ?? err)}`);
|
|
128788
|
+
}
|
|
128789
|
+
}
|
|
128790
|
+
}
|
|
128791
|
+
const targetGuid = patch.due || patch.blocked ? createdGuid ?? deps.store.get(patch.threadId)?.taskGuid ?? patch.declaredGuid : void 0;
|
|
128792
|
+
if (patch.due) {
|
|
128793
|
+
if (!targetGuid) {
|
|
128794
|
+
outcomes.push("due \u58F0\u660E\u88AB\u5FFD\u7565\uFF1A\u672C\u8BDD\u9898\u6CA1\u6709\u5DF2\u8BA4\u9886/\u65B0\u5EFA\u7684\u4EFB\u52A1");
|
|
128795
|
+
} else {
|
|
128796
|
+
const due = parseDueInput(patch.due);
|
|
128797
|
+
if (!due) {
|
|
128798
|
+
outcomes.push(`due "${patch.due}" \u65E0\u6CD5\u89E3\u6790\uFF0C\u672A\u6539\u671F`);
|
|
128799
|
+
} else {
|
|
128800
|
+
try {
|
|
128801
|
+
await deps.client.patchDue(targetGuid, due);
|
|
128802
|
+
const line = `\u23F0 \u622A\u6B62\u6539\u4E3A ${formatDueForComment(due)}${patch.dueReason ? `\uFF1A${patch.dueReason}` : ""}`;
|
|
128803
|
+
await deps.client.addComment(targetGuid, line).catch((err) => {
|
|
128804
|
+
console.warn(`[tasklist.declare] due comment failed (due itself applied):`, err);
|
|
128805
|
+
});
|
|
128806
|
+
outcomes.push(`\u5DF2\u6539\u671F ${formatDueForComment(due)}${patch.dueReason ? `\uFF08${patch.dueReason}\uFF09` : ""}`);
|
|
128807
|
+
} catch (err) {
|
|
128808
|
+
outcomes.push(`\u6539\u671F\u5931\u8D25\uFF1A${String(err.message ?? err)}`);
|
|
128809
|
+
}
|
|
128810
|
+
}
|
|
128811
|
+
}
|
|
128812
|
+
}
|
|
128813
|
+
if (patch.blocked) {
|
|
128814
|
+
if (!targetGuid) {
|
|
128815
|
+
outcomes.push("blocked \u58F0\u660E\u88AB\u5FFD\u7565\uFF1A\u672C\u8BDD\u9898\u6CA1\u6709\u5DF2\u8BA4\u9886/\u65B0\u5EFA\u7684\u4EFB\u52A1");
|
|
128816
|
+
} else {
|
|
128817
|
+
try {
|
|
128818
|
+
await deps.client.addComment(targetGuid, `\u{1F6A7} \u963B\u585E\uFF0C\u9700\u8981\u4EBA\u5904\u7406\uFF1A${patch.blocked}`);
|
|
128819
|
+
outcomes.push("\u963B\u585E\u5DF2\u843D\u4EFB\u52A1\u8BC4\u8BBA\uFF08\u5173\u6CE8\u4EBA\u5C06\u6536\u5230\u901A\u77E5\uFF09");
|
|
128820
|
+
} catch (err) {
|
|
128821
|
+
outcomes.push(`\u963B\u585E\u8BC4\u8BBA\u5931\u8D25\uFF1A${String(err.message ?? err)}`);
|
|
128822
|
+
}
|
|
128823
|
+
}
|
|
128824
|
+
}
|
|
128825
|
+
return { createdGuid, outcomes };
|
|
128826
|
+
}
|
|
128827
|
+
|
|
128586
128828
|
// src/tasklist/commentPoller.ts
|
|
128587
128829
|
var DEFAULT_INTERVAL_MS = 6e4;
|
|
128588
128830
|
var DEFAULT_JITTER_MS = 1e4;
|
|
@@ -130159,6 +130401,7 @@ async function runV2Mode({
|
|
|
130159
130401
|
let effectiveTaskHandleTasklistGuid;
|
|
130160
130402
|
let taskHandleLifecycle;
|
|
130161
130403
|
let taskHandleClaim;
|
|
130404
|
+
let taskHandleDeclare;
|
|
130162
130405
|
let taskHandleClaimedLookup;
|
|
130163
130406
|
let taskHandleClaimGuidLookup;
|
|
130164
130407
|
let taskGuidClaimedByOtherBot;
|
|
@@ -130194,6 +130437,12 @@ async function runV2Mode({
|
|
|
130194
130437
|
const taskHandleStore = await TaskHandleStore.load(resolveTaskHandlesPath(bot.id));
|
|
130195
130438
|
allTaskHandleStores.push({ botId: bot.id, store: taskHandleStore });
|
|
130196
130439
|
taskHandleLifecycle = (patch) => applyTaskHandleWriteback(patch, { store: taskHandleStore, client: taskListClient });
|
|
130440
|
+
taskHandleDeclare = (patch) => applyTaskHandleDeclarations(patch, {
|
|
130441
|
+
store: taskHandleStore,
|
|
130442
|
+
client: taskListClient,
|
|
130443
|
+
tasklistGuid: effectiveTaskHandleTasklistGuid,
|
|
130444
|
+
botName: bot.name
|
|
130445
|
+
});
|
|
130197
130446
|
taskHandleClaim = async (patch) => {
|
|
130198
130447
|
const result = await taskHandleStore.claim(patch);
|
|
130199
130448
|
if (!result.claimed) {
|
|
@@ -130457,6 +130706,7 @@ async function runV2Mode({
|
|
|
130457
130706
|
},
|
|
130458
130707
|
taskHandleLifecycle,
|
|
130459
130708
|
taskHandleClaim,
|
|
130709
|
+
taskHandleDeclare,
|
|
130460
130710
|
taskHandleClaimedLookup,
|
|
130461
130711
|
taskHandleClaimGuidLookup,
|
|
130462
130712
|
taskGuidClaimedByOtherBot,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "larkway",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.59",
|
|
4
4
|
"description": "Thin bridge: Feishu thread to local Claude Code CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Chuck Wu (chuckwu0)",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"type": "module",
|
|
19
19
|
"bin": {
|
|
20
|
-
"larkway": "bin/larkway"
|
|
20
|
+
"larkway": "bin/larkway.mjs"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"start": "tsx src/main.ts",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"files": [
|
|
56
56
|
"dist/",
|
|
57
|
-
"bin/larkway",
|
|
57
|
+
"bin/larkway.mjs",
|
|
58
58
|
"README.md"
|
|
59
59
|
]
|
|
60
60
|
}
|
package/bin/larkway
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# larkway CLI shim — resolves the package root from this script's own location
|
|
3
|
-
# so it works whether invoked via `bin/larkway`, an absolute path, or a symlink
|
|
4
|
-
# on PATH (e.g. after `npm i -g`).
|
|
5
|
-
#
|
|
6
|
-
# Startup priority:
|
|
7
|
-
# 1. dist/cli/index.js exists → `node dist/cli/index.js` (installed package / post-build)
|
|
8
|
-
# 2. otherwise → `npx tsx src/cli/index.ts` (fresh clone dev mode, no build)
|
|
9
|
-
set -euo pipefail
|
|
10
|
-
SOURCE="${BASH_SOURCE[0]}"
|
|
11
|
-
while [ -h "$SOURCE" ]; do
|
|
12
|
-
DIR="$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd)"
|
|
13
|
-
SOURCE="$(readlink "$SOURCE")"
|
|
14
|
-
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
|
|
15
|
-
done
|
|
16
|
-
REPO_ROOT="$(cd -P "$(dirname "$SOURCE")/.." >/dev/null 2>&1 && pwd)"
|
|
17
|
-
|
|
18
|
-
DIST_ENTRY="$REPO_ROOT/dist/cli/index.js"
|
|
19
|
-
|
|
20
|
-
if [ -f "$DIST_ENTRY" ]; then
|
|
21
|
-
exec node "$DIST_ENTRY" "$@"
|
|
22
|
-
else
|
|
23
|
-
# Dev fallback: run TypeScript source directly via tsx.
|
|
24
|
-
exec npx tsx "$REPO_ROOT/src/cli/index.ts" "$@"
|
|
25
|
-
fi
|