dsh-task-ledger 0.0.125 → 0.0.127

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 (51) hide show
  1. package/dist/client.js +21192 -11429
  2. package/dist/client.js.map +1 -1
  3. package/dist/host/errors.d.ts +9 -3
  4. package/dist/host/errors.js +10 -0
  5. package/dist/host/repositories/cache.d.ts +2 -2
  6. package/dist/host/repositories/dirs.repository.d.ts +37 -0
  7. package/dist/host/repositories/dirs.repository.js +97 -0
  8. package/dist/host/repositories/history.repository.d.ts +20 -0
  9. package/dist/host/repositories/history.repository.js +63 -0
  10. package/dist/host/repositories/identity.repository.d.ts +53 -3
  11. package/dist/host/repositories/identity.repository.js +105 -10
  12. package/dist/host/repositories/ledger.repository.d.ts +0 -8
  13. package/dist/host/repositories/ledger.repository.js +3 -20
  14. package/dist/host/repositories/runner.d.ts +29 -0
  15. package/dist/host/repositories/runner.js +66 -4
  16. package/dist/host/repositories/stats.repository.js +2 -2
  17. package/dist/host/repositories/task.repository.d.ts +12 -0
  18. package/dist/host/repositories/task.repository.js +63 -0
  19. package/dist/host/repositories/trend.repository.d.ts +15 -5
  20. package/dist/host/repositories/trend.repository.js +38 -11
  21. package/dist/host/routes.d.ts +13 -1
  22. package/dist/host/routes.js +379 -12
  23. package/dist/host/services/continue.service.d.ts +126 -0
  24. package/dist/host/services/continue.service.js +138 -0
  25. package/dist/host/services/dirs.service.d.ts +34 -0
  26. package/dist/host/services/dirs.service.js +66 -0
  27. package/dist/host/services/history.service.d.ts +14 -0
  28. package/dist/host/services/history.service.js +172 -0
  29. package/dist/host/services/identity.service.d.ts +53 -0
  30. package/dist/host/services/identity.service.js +89 -0
  31. package/dist/host/services/stats.service.d.ts +2 -2
  32. package/dist/host/services/stats.service.js +23 -6
  33. package/dist/host/services/task.service.d.ts +15 -0
  34. package/dist/host/services/task.service.js +15 -0
  35. package/dist/host/services/trend.service.d.ts +2 -2
  36. package/dist/host/services/trend.service.js +5 -5
  37. package/dist/host/services/workspaces.service.d.ts +10 -0
  38. package/dist/host/services/workspaces.service.js +28 -0
  39. package/dist/host/types.d.ts +108 -6
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.js +31 -3
  42. package/package.json +2 -2
  43. package/skill/SKILL.md +13 -13
  44. package/skill/package.json +1 -1
  45. package/skill/references/init-identity.md +1 -1
  46. package/skill/references/troubleshooting.md +1 -1
  47. package/skill/scripts/bin/ledger-darwin-arm64 +0 -0
  48. package/skill/scripts/bin/ledger-linux-amd64 +0 -0
  49. package/skill/scripts/bin/ledger-windows-amd64.exe +0 -0
  50. package/dist/host/workspace.d.ts +0 -20
  51. package/dist/host/workspace.js +0 -17
@@ -8,7 +8,13 @@
8
8
  * 防止「台账程序根本没装」被集体降级成一片绿色空板——那等于界面撒谎。
9
9
  */
10
10
  /** 机器可读错误码(只增不改;`LEDGER_` 前缀避开宿主错误码空间)。 */
11
- export type LedgerErrorCode = "LEDGER_SCRIPT_UNAVAILABLE" | "LEDGER_SCRIPT_TIMEOUT" | "LEDGER_SCRIPT_FAILED" | "LEDGER_SCRIPT_BAD_JSON" | "LEDGER_DATA_INVALID";
11
+ export type LedgerErrorCode = "LEDGER_SCRIPT_UNAVAILABLE" | "LEDGER_SCRIPT_TIMEOUT" | "LEDGER_SCRIPT_FAILED" | "LEDGER_SCRIPT_BAD_JSON" | "LEDGER_DATA_INVALID" | "LEDGER_IDENTITY_MISSING";
12
+ /** 这台机器还没有身份。message 仍是脚本原文(可直接上界面),码用来分流。 */
13
+ export declare class IdentityMissingError extends Error {
14
+ readonly code: LedgerErrorCode;
15
+ readonly workspaceId: string | null;
16
+ constructor(message: string, workspaceId?: string | null);
17
+ }
12
18
  /** 台账数据形状不符(容器级校验失败)。message 带工作区名 + 人话,可直接上界面。 */
13
19
  export declare class LedgerDataError extends Error {
14
20
  readonly code: LedgerErrorCode;
@@ -20,7 +26,7 @@ export interface DegradedInfo {
20
26
  readonly workspaceId: string;
21
27
  readonly workspaceTitle: string;
22
28
  /** 哪个读挂了(skill 子命令;趋势走 `stats --trend`,单独一档便于区分)。 */
23
- readonly op: "list" | "owners" | "trend";
29
+ readonly op: "list" | "owners" | "trend" | "history";
24
30
  readonly code: LedgerErrorCode;
25
31
  /** 人话,可直接上界面(真源:脚本 stderr / records 校验文案)。 */
26
32
  readonly message: string;
@@ -36,7 +42,7 @@ export declare function messageOf(error: unknown): string;
36
42
  export declare function settleWorkspaces<W extends {
37
43
  id: string;
38
44
  title: string;
39
- }, T, R>(workspaces: readonly W[], op: "list" | "owners", read: (workspace: W) => Promise<T>, merge: (rows: readonly T[]) => R): Promise<{
45
+ }, T, R>(workspaces: readonly W[], op: "list" | "owners" | "history", read: (workspace: W) => Promise<T>, merge: (rows: readonly T[]) => R): Promise<{
40
46
  data: R;
41
47
  degraded: readonly DegradedInfo[];
42
48
  }>;
@@ -7,6 +7,16 @@
7
7
  * - **全局失败**(所有工作区都读不到 / 编程错误):照旧抛,路由转 500 + 人话。
8
8
  * 防止「台账程序根本没装」被集体降级成一片绿色空板——那等于界面撒谎。
9
9
  */
10
+ /** 这台机器还没有身份。message 仍是脚本原文(可直接上界面),码用来分流。 */
11
+ export class IdentityMissingError extends Error {
12
+ code = "LEDGER_IDENTITY_MISSING";
13
+ workspaceId;
14
+ constructor(message, workspaceId = null) {
15
+ super(message);
16
+ this.name = "IdentityMissingError";
17
+ this.workspaceId = workspaceId;
18
+ }
19
+ }
10
20
  /** 台账数据形状不符(容器级校验失败)。message 带工作区名 + 人话,可直接上界面。 */
11
21
  export class LedgerDataError extends Error {
12
22
  code = "LEDGER_DATA_INVALID";
@@ -11,8 +11,8 @@
11
11
  /** 缓存有效期(2026-10-18 拍板 1.5s:够挡同一屏的重复读,短到不会让人看到旧数据)。 */
12
12
  export declare const CACHE_TTL_MS = 1500;
13
13
  /** 缓存的操作种类(= skill 脚本子命令;worklog 不进缓存故不在此列)。
14
- * `history` 已从插件侧消失(2026-10-29:本周 / 上周完成改取 `stats --trend` 的桶),不再单列。 */
15
- export type CacheOp = "list" | "stats" | "owners";
14
+ * `history` 供历史页顶部日趋势使用(2026-10-29 重新接入)。 */
15
+ export type CacheOp = "history" | "list" | "stats" | "owners";
16
16
  /** 缓存键:`op:workspace:filter` 三条信息都要,否则不同筛选 / 不同工作区会串味。 */
17
17
  export declare function cacheKey(workspaceId: string, op: CacheOp, detail: string): string;
18
18
  /**
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 机器级台账目录清单(`~/.taskledger/ledgers.json`,真源在 skill 侧)。
3
+ *
4
+ * **写只走 skill**(`dirs --add`),插件只读 —— 将来的其它看板(网页 / APP)也照此办理,
5
+ * 免得两套写法把同一个文件写坏(2026-11-01 用户拍板)。
6
+ */
7
+ export interface LedgerDir {
8
+ readonly path: string;
9
+ readonly name: string;
10
+ readonly hidden: boolean;
11
+ }
12
+ /**
13
+ * 读清单。skill 侧会顺手清掉失效条目(目录没了 / 已经不是台账)再返回。
14
+ * 损坏的清单按空处理,不让「看台账」这件事失败。
15
+ */
16
+ export declare function readDirs(): Promise<LedgerDir[]>;
17
+ /** 看板上显示 / 不显示这一个台账(只动清单,目录与台账文件一个都不碰)。 */
18
+ export declare function setDirHidden(path: string, hidden: boolean): Promise<LedgerDir[]>;
19
+ /** 改台账名;空名 = 回落目录名(skill 口径)。 */
20
+ export declare function renameDir(path: string, name: string): Promise<LedgerDir[]>;
21
+ /** 从清单移除(不是删目录:台账数据与文件全部原样保留)。 */
22
+ export declare function removeDir(path: string): Promise<LedgerDir[]>;
23
+ /**
24
+ * 登记一个目录(幂等)。不是台账根时 skill 返回 `added:false` + 原因,不算错误。
25
+ * 返回登记后的整份清单,调用方不必再读一次。
26
+ */
27
+ export declare function addDir(path: string, name: string): Promise<{
28
+ readonly dirs: LedgerDir[];
29
+ readonly added: boolean;
30
+ readonly reason: string;
31
+ }>;
32
+ /**
33
+ * 一级子目录里的台账根。用户 2026-11-03:只知道「这个项目有台账」的人会选它的**直属父目录**,
34
+ * 这种选择也算有效 —— 父目录本身不进清单(清单只存台账根),登记它下面这些。
35
+ * 目录读不了 / 没有台账 ⇒ 空数组,由调用方出提示。
36
+ */
37
+ export declare function ledgerChildrenOf(path: string): Promise<string[]>;
@@ -0,0 +1,97 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { failureOf, runLedgerScript } from "./runner.js";
4
+ function parseDirs(data) {
5
+ const rows = Array.isArray(data.dirs) ? data.dirs : [];
6
+ const out = [];
7
+ for (const row of rows) {
8
+ if (typeof row?.path !== "string" || row.path === "")
9
+ continue;
10
+ out.push({
11
+ path: row.path,
12
+ name: typeof row.name === "string" ? row.name : "",
13
+ hidden: row.hidden === true,
14
+ });
15
+ }
16
+ return out;
17
+ }
18
+ /**
19
+ * 读清单。skill 侧会顺手清掉失效条目(目录没了 / 已经不是台账)再返回。
20
+ * 损坏的清单按空处理,不让「看台账」这件事失败。
21
+ */
22
+ export async function readDirs() {
23
+ // --all:必须拿到隐藏项,否则「这些路径已登记过」判不出来,每轮都会把它们当新目录反复补登记
24
+ const result = await runLedgerScript("dirs", ["--all", "--json"]);
25
+ if (!result.ok)
26
+ throw failureOf(result, "读台账目录清单失败:");
27
+ return parseDirs(result.data);
28
+ }
29
+ /**
30
+ * 写动作的公共出口:skill 的写命令**恒回全量清单**(含隐藏项,`actions/dirs.go:201`),
31
+ * 调用方拿它就地换掉表格,不必再读一次。
32
+ */
33
+ async function writeDirs(args) {
34
+ const result = await runLedgerScript("dirs", [...args, "--json"]);
35
+ if (!result.ok)
36
+ throw failureOf(result, "改台账目录清单失败:");
37
+ return parseDirs(result.data);
38
+ }
39
+ /** 看板上显示 / 不显示这一个台账(只动清单,目录与台账文件一个都不碰)。 */
40
+ export function setDirHidden(path, hidden) {
41
+ return writeDirs(["--dir", path, hidden ? "--hide" : "--unhide"]);
42
+ }
43
+ /** 改台账名;空名 = 回落目录名(skill 口径)。 */
44
+ export function renameDir(path, name) {
45
+ return writeDirs(["--dir", path, "--rename", name]);
46
+ }
47
+ /** 从清单移除(不是删目录:台账数据与文件全部原样保留)。 */
48
+ export function removeDir(path) {
49
+ return writeDirs(["--dir", path, "--remove"]);
50
+ }
51
+ /**
52
+ * 登记一个目录(幂等)。不是台账根时 skill 返回 `added:false` + 原因,不算错误。
53
+ * 返回登记后的整份清单,调用方不必再读一次。
54
+ */
55
+ export async function addDir(path, name) {
56
+ const args = ["--dir", path, "--add", "--json"];
57
+ if (name !== "")
58
+ args.push("--name", name);
59
+ const result = await runLedgerScript("dirs", args);
60
+ if (!result.ok)
61
+ throw failureOf(result, "登记台账目录失败:");
62
+ return {
63
+ dirs: parseDirs(result.data),
64
+ added: result.data.added === true,
65
+ reason: typeof result.data.reason === "string" ? result.data.reason : "",
66
+ };
67
+ }
68
+ /** 目录下**直接**含 `ledger/` ⇒ 台账根(与 skill `root` 同一判据:`core/roots.go:35`)。 */
69
+ async function hasLedger(dir) {
70
+ try {
71
+ return (await stat(join(dir, "ledger"))).isDirectory();
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ /**
78
+ * 一级子目录里的台账根。用户 2026-11-03:只知道「这个项目有台账」的人会选它的**直属父目录**,
79
+ * 这种选择也算有效 —— 父目录本身不进清单(清单只存台账根),登记它下面这些。
80
+ * 目录读不了 / 没有台账 ⇒ 空数组,由调用方出提示。
81
+ */
82
+ export async function ledgerChildrenOf(path) {
83
+ let names;
84
+ try {
85
+ names = await readdir(path);
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ const found = [];
91
+ for (const name of names.sort()) {
92
+ const child = join(path, name);
93
+ if (await hasLedger(child))
94
+ found.push(child);
95
+ }
96
+ return found;
97
+ }
@@ -0,0 +1,20 @@
1
+ import type { HistoryRecord, LedgerWorkspace } from "../types.js";
2
+ /**
3
+ * 历史明细(skill `history --json`):按**结案时间**过滤,新→旧。
4
+ *
5
+ * 分页口径:skill 没有 offset,只有时间窗 + `--limit` ⇒ 下一页用「本页最后一条的
6
+ * `finished`」当新的 `--until`(含边界,跨页会重叠几条,由上层按 key 去重)。
7
+ */
8
+ export interface HistoryQueryOptions {
9
+ /** `YYYY-MM-DD HH:MM:SS`(与存盘同形的串,脚本直接字符串比较)。 */
10
+ readonly since: string;
11
+ readonly until: string;
12
+ /** 不给 = 整月全量(2026-11-01 起历史列表按整月取,不再封顶)。 */
13
+ readonly limit?: number;
14
+ /** 执行人 uuid;不给 = 不限。 */
15
+ readonly owner?: string;
16
+ }
17
+ export declare function readHistory(workspace: LedgerWorkspace, options: HistoryQueryOptions): Promise<{
18
+ records: HistoryRecord[];
19
+ capped: boolean;
20
+ }>;
@@ -0,0 +1,63 @@
1
+ import path from "node:path";
2
+ import { cached, cacheKey } from "./cache.js";
3
+ import { failureOf, runLedgerScript } from "./runner.js";
4
+ /** 脚本一行 → 带工作区身份的记录(缺字段一律空串,不编造)。 */
5
+ function toRecord(raw, workspace) {
6
+ const src = typeof raw.src === "string" ? raw.src : "";
7
+ const line = typeof raw.line === "number" ? raw.line : 0;
8
+ const text = (value) => typeof value === "string" ? value : "";
9
+ return {
10
+ // ⚠️ 必须带工作区:各仓的 src 都是 `ledger/history/YYYYMM.md`、line 各自编号,
11
+ // 只用 src:line 会让不同工作区的记录撞成同一个 key,合并去重时被当成重复吞掉。
12
+ key: `${workspace.id}:${src}:${String(line)}`,
13
+ workspaceId: workspace.id,
14
+ workspaceTitle: workspace.title,
15
+ title: text(raw.title),
16
+ summary: text(raw.summary),
17
+ category: text(raw.category),
18
+ tags: Array.isArray(raw.tags)
19
+ ? raw.tags.filter((tag) => typeof tag === "string")
20
+ : [],
21
+ result: text(raw.result),
22
+ owner: text(raw.owner),
23
+ ownerName: text(raw.owner_name),
24
+ creator: text(raw.creator),
25
+ creatorName: text(raw.creator_name),
26
+ created: text(raw.created),
27
+ finished: text(raw.finished),
28
+ // worklog 是相对**历史文件所在目录**的串(`../worklog/202610/x.md`)⇒ 这一步算成绝对路径,
29
+ // 前端拿去直接开预览(页面不许自己拼台账布局)。空 = 这行没有过程文档。
30
+ filePath: text(raw.worklog) === ""
31
+ ? ""
32
+ : path.join(workspace.path, "ledger", "history", text(raw.worklog)),
33
+ };
34
+ }
35
+ export async function readHistory(workspace, options) {
36
+ const args = [
37
+ "--repo",
38
+ workspace.path,
39
+ "--json",
40
+ "--since",
41
+ options.since,
42
+ "--until",
43
+ options.until,
44
+ ];
45
+ if (options.limit !== undefined)
46
+ args.push("--limit", String(options.limit));
47
+ if (options.owner !== undefined)
48
+ args.push("--owner", options.owner);
49
+ return await cached(cacheKey(workspace.id, "history", args.slice(2).join("\u0001")), async () => {
50
+ const result = await runLedgerScript("history", args);
51
+ if (!result.ok)
52
+ throw failureOf(result, `${workspace.title}: `, workspace.id);
53
+ const rows = result.data.records;
54
+ if (!Array.isArray(rows))
55
+ throw new Error(`${workspace.title}: history 输出缺 records 数组`);
56
+ return {
57
+ records: rows
58
+ .filter((row) => row !== null && typeof row === "object")
59
+ .map((row) => toRecord(row, workspace)),
60
+ capped: result.data.capped === true,
61
+ };
62
+ });
63
+ }
@@ -1,10 +1,60 @@
1
1
  import type { LedgerWorkspace } from "../types.js";
2
2
  /**
3
- * 本机身份 uuid —— 走 skill `whoami --json`(**不再自己读 identity.json**:
3
+ * 本机身份 —— 走 skill `whoami --json` / `switch --json`(**不自己读 identity.json**:
4
4
  * `$TASK_LEDGER_HOME` / 默认目录 / 文件格式都是 skill 的知识,插件不复制第二份)。
5
5
  *
6
- * **只取 `uuid`**:`name` 是冗余显示字段,不参与任何判定(按名字比 owner 永远比不到)。
7
- * 未初始化时 skill 回 `{"uuid":null}`(退 0),这里同样回 null ⇒ 「我的」按 0 计(不谎报)。
6
+ * 未初始化时 skill 回 `{"uuid":null,"name":null}`(退 0)⇒ 这里同样回 null ⇒ 「我的」按 0 计(不谎报)。
8
7
  * 程序起不来看不出身份,也只回 null。
9
8
  */
9
+ /** 本机身份;`uuid === null` = 这台机器还没有身份。 */
10
+ export interface IdentityView {
11
+ readonly uuid: string | null;
12
+ readonly name: string | null;
13
+ }
14
+ /** 换身份:建全新身份 / 认领某个工作区名册里已有的人 / 改自己的昵称。 */
15
+ export type SwitchInput = {
16
+ readonly action: "new";
17
+ readonly name: string;
18
+ } | {
19
+ readonly action: "claim";
20
+ readonly uuid: string;
21
+ readonly repo: string;
22
+ } | {
23
+ readonly action: "rename";
24
+ readonly name: string;
25
+ };
26
+ /** 改昵称时某一本台账的结果(skill `switch --rename --json` 的 `dirs[]`)。 */
27
+ export interface RenameDirResult {
28
+ readonly dir: string;
29
+ /** changed = 改了且提交推送成功;commit-only = 改了但只提交在本地;skipped = 没改;failed = 出错。 */
30
+ readonly status: "changed" | "commit-only" | "skipped" | "failed";
31
+ readonly reason: string;
32
+ }
33
+ /** 改昵称的回执:新昵称 + 每本台账各自的结果(跨仓库做不到全成或全败)。 */
34
+ export interface RenameView {
35
+ readonly uuid: string;
36
+ readonly name: string;
37
+ readonly previous: string;
38
+ readonly dirs: readonly RenameDirResult[];
39
+ }
40
+ export declare function readIdentity(): Promise<IdentityView>;
41
+ /** 只要 uuid(看板行级的「是不是我的」以 skill 的 `mine` 为准,这里只备查)。 */
10
42
  export declare function getCurrentIdentity(_workspace?: LedgerWorkspace): Promise<string | null>;
43
+ /**
44
+ * 换身份。调的是隐藏命令 `switch`(不进 SKILL.md):它允许覆盖已有身份、不受真终端限制,
45
+ * 正是 `init` 做不到的两件事。失败把人话抛给路由(→ 500 → 界面原地显示)。
46
+ */
47
+ export declare function switchIdentity(input: SwitchInput): Promise<IdentityView | RenameView>;
48
+ /** 从一本台账的名册里摘掉一个人(skill `forget --json`)。 */
49
+ export interface ForgetOutcome {
50
+ /** false = 分片里还有任务,这本台账没删。 */
51
+ readonly removed: boolean;
52
+ readonly name: string;
53
+ readonly tasks: number;
54
+ }
55
+ export declare function forgetOwner(repo: string, uuid: string): Promise<ForgetOutcome>;
56
+ /**
57
+ * 清掉本机身份(skill `logout`):台账一个字节都不动。
58
+ * 只删 ~/.taskledger/identity.json —— 路径归 skill 管,插件不复制这份知识。
59
+ */
60
+ export declare function logout(repo: string): Promise<void>;
@@ -1,16 +1,111 @@
1
1
  import { runLedgerScript } from "./runner.js";
2
2
  /**
3
- * 本机身份 uuid —— 走 skill `whoami --json`(**不再自己读 identity.json**:
4
- * `$TASK_LEDGER_HOME` / 默认目录 / 文件格式都是 skill 的知识,插件不复制第二份)。
5
- *
6
- * **只取 `uuid`**:`name` 是冗余显示字段,不参与任何判定(按名字比 owner 永远比不到)。
7
- * 未初始化时 skill 回 `{"uuid":null}`(退 0),这里同样回 null ⇒ 「我的」按 0 计(不谎报)。
8
- * 程序起不来看不出身份,也只回 null。
3
+ * 改昵称的超时:要逐本台账走「取最新 → 改 → 提交 → 推送」,
4
+ * 一本就可能几秒,十个目录轻松过一分钟 —— 绝不能吃默认的 10s。
9
5
  */
10
- export async function getCurrentIdentity(_workspace) {
6
+ const RENAME_TIMEOUT_MS = 180_000;
7
+ /** 删人同样逐本台账走 git(取最新 → 改 → 提交 → 推送),10s 默认值不够。 */
8
+ const FORGET_TIMEOUT_MS = 120_000;
9
+ const UNKNOWN_IDENTITY = { uuid: null, name: null };
10
+ function viewOf(data) {
11
+ const uuid = typeof data.uuid === "string" ? data.uuid.trim() : "";
12
+ const name = typeof data.name === "string" ? data.name.trim() : "";
13
+ return {
14
+ uuid: uuid === "" ? null : uuid.toLowerCase(),
15
+ name: name === "" ? null : name,
16
+ };
17
+ }
18
+ export async function readIdentity() {
11
19
  const result = await runLedgerScript("whoami", ["--json"]);
12
20
  if (!result.ok)
13
- return null;
14
- const uuid = typeof result.data.uuid === "string" ? result.data.uuid.trim() : "";
15
- return uuid === "" ? null : uuid.toLowerCase();
21
+ return UNKNOWN_IDENTITY;
22
+ return viewOf(result.data);
23
+ }
24
+ /** 只要 uuid(看板行级的「是不是我的」以 skill 的 `mine` 为准,这里只备查)。 */
25
+ export async function getCurrentIdentity(_workspace) {
26
+ return (await readIdentity()).uuid;
27
+ }
28
+ /**
29
+ * 换身份。调的是隐藏命令 `switch`(不进 SKILL.md):它允许覆盖已有身份、不受真终端限制,
30
+ * 正是 `init` 做不到的两件事。失败把人话抛给路由(→ 500 → 界面原地显示)。
31
+ */
32
+ export async function switchIdentity(input) {
33
+ const args = ["--json"];
34
+ if (input.action === "rename") {
35
+ args.push("--rename", input.name);
36
+ }
37
+ else if (input.action === "new") {
38
+ args.push("--new", "--name", input.name);
39
+ }
40
+ else {
41
+ args.push("--claim", input.uuid, "--repo", input.repo);
42
+ }
43
+ const result = await runLedgerScript("switch", args, {
44
+ timeoutMs: input.action === "rename" ? RENAME_TIMEOUT_MS : undefined,
45
+ });
46
+ if (!result.ok)
47
+ throw new Error(result.error);
48
+ if (input.action === "rename") {
49
+ const identity = viewOf(result.data);
50
+ if (identity.uuid === null || identity.name === null)
51
+ throw new Error("改昵称的回执里没有身份(脚本输出异常)");
52
+ return {
53
+ uuid: identity.uuid,
54
+ name: identity.name,
55
+ previous: typeof result.data.previous === "string" ? result.data.previous : "",
56
+ dirs: dirsOf(result.data.dirs),
57
+ };
58
+ }
59
+ return viewOf(result.data);
60
+ }
61
+ export async function forgetOwner(repo, uuid) {
62
+ // 删人也走一遍「写前取最新 → 改 → 提交 → 推送」,一次网络往返就可能几秒,
63
+ // 默认的 10s 会把它判超时(脚本被 kill 时连分片都还没删)。
64
+ //
65
+ // **--keep-identity**:界面删自己会逐本台账各跑一次,谁先跑完谁把 identity.json 删了,
66
+ // 后面几本就会报「这台机器还没有初始化身份」(用户 2026-11-02 实测)。清身份留到最后一步。
67
+ const result = await runLedgerScript("forget", ["--id", uuid, "--keep-identity", "--repo", repo, "--json"], {
68
+ timeoutMs: FORGET_TIMEOUT_MS,
69
+ });
70
+ if (!result.ok)
71
+ throw new Error(result.error);
72
+ return {
73
+ removed: result.data.removed === true,
74
+ name: typeof result.data.name === "string" ? result.data.name : "",
75
+ tasks: typeof result.data.tasks === "number" ? result.data.tasks : 0,
76
+ };
77
+ }
78
+ /**
79
+ * 清掉本机身份(skill `logout`):台账一个字节都不动。
80
+ * 只删 ~/.taskledger/identity.json —— 路径归 skill 管,插件不复制这份知识。
81
+ */
82
+ export async function logout(repo) {
83
+ const result = await runLedgerScript("logout", ["--json", "--repo", repo], { timeoutMs: 30_000 });
84
+ if (!result.ok)
85
+ throw new Error(result.error);
86
+ }
87
+ /** `dirs[]` 原样搬过来(形状不对的行直接丢掉,不编造)。 */
88
+ function dirsOf(raw) {
89
+ if (!Array.isArray(raw))
90
+ return [];
91
+ const out = [];
92
+ for (const row of raw) {
93
+ if (row === null || typeof row !== "object")
94
+ continue;
95
+ const item = row;
96
+ if (typeof item.dir !== "string")
97
+ continue;
98
+ const status = item.status;
99
+ if (status !== "changed" &&
100
+ status !== "commit-only" &&
101
+ status !== "skipped" &&
102
+ status !== "failed")
103
+ continue;
104
+ out.push({
105
+ dir: item.dir,
106
+ status,
107
+ reason: typeof item.reason === "string" ? item.reason : "",
108
+ });
109
+ }
110
+ return out;
16
111
  }
@@ -5,14 +5,6 @@ import type { LedgerFilter, LedgerWorkspace, TaskRow, WorkspaceOwners } from "..
5
5
  * 参数名以 `skill/shell/cli.go` 的 `list` 白名单为准,拼错一个就让 argparse 退 2、整块列表 500。
6
6
  */
7
7
  export declare function filterArgs(filter: LedgerFilter): string[];
8
- /**
9
- * 台账工作区探测:走 skill `root --json`(判据是「这个目录直接含 `ledger/`」,
10
- * 与脚本定位工作区根同一口径;有没有 `pool.json` 不影响)。
11
- *
12
- * 曾经的 `<path>/ledger/pool.json` 直查已废——布局知识只留 skill 一份。
13
- * 程序起不来时抛错(不静默把工作区藏掉)。
14
- */
15
- export declare function hasLedger(workspacePath: string): Promise<boolean>;
16
8
  /**
17
9
  * 在办任务(包 `list --json`;结果过 records 入口校验后打上 workspace)。
18
10
  * 脚本失败 / 形状不符抛错——由 service 层捕获进 degraded,路由层转 500 + 人话,
@@ -1,6 +1,6 @@
1
1
  import { cached, cacheKey } from "./cache.js";
2
2
  import { parseTaskList } from "./records.js";
3
- import { runLedgerScript } from "./runner.js";
3
+ import { failureOf, runLedgerScript } from "./runner.js";
4
4
  /**
5
5
  * LedgerFilter → skill 脚本参数(只拼调用方给了的;undefined 一律不拼,
6
6
  * 让脚本走自己的默认——插件不替脚本决定默认值)。
@@ -25,23 +25,6 @@ export function filterArgs(filter) {
25
25
  args.push("--until", filter.until);
26
26
  return args;
27
27
  }
28
- /**
29
- * 台账工作区探测:走 skill `root --json`(判据是「这个目录直接含 `ledger/`」,
30
- * 与脚本定位工作区根同一口径;有没有 `pool.json` 不影响)。
31
- *
32
- * 曾经的 `<path>/ledger/pool.json` 直查已废——布局知识只留 skill 一份。
33
- * 程序起不来时抛错(不静默把工作区藏掉)。
34
- */
35
- export async function hasLedger(workspacePath) {
36
- const result = await runLedgerScript("root", [
37
- "--dir",
38
- workspacePath,
39
- "--json",
40
- ]);
41
- if (!result.ok)
42
- throw new Error(`台账根检测失败(${workspacePath}):${result.error}`);
43
- return result.data.root === true;
44
- }
45
28
  /**
46
29
  * 在办任务(包 `list --json`;结果过 records 入口校验后打上 workspace)。
47
30
  * 脚本失败 / 形状不符抛错——由 service 层捕获进 degraded,路由层转 500 + 人话,
@@ -56,7 +39,7 @@ export async function readTasks(workspace, filter = {}) {
56
39
  ...filterArgs(filter),
57
40
  ]);
58
41
  if (!result.ok)
59
- throw new Error(`${workspace.title}: ${result.error}`);
42
+ throw failureOf(result, `${workspace.title}: `, workspace.id);
60
43
  return parseTaskList(result.data, workspace);
61
44
  });
62
45
  }
@@ -68,7 +51,7 @@ export async function readOwners(workspace) {
68
51
  return await cached(cacheKey(workspace.id, "owners", ""), async () => {
69
52
  const result = await runLedgerScript("owners", ["--repo", workspace.path, "--json"]);
70
53
  if (!result.ok)
71
- throw new Error(`${workspace.title}: ${result.error}`);
54
+ throw failureOf(result, `${workspace.title}: `, workspace.id);
72
55
  const owners = Object.entries(result.data.owners ?? {}).map(([uuid, value]) => ({
73
56
  uuid,
74
57
  name: typeof value.name === "string" && value.name.trim() !== ""
@@ -1,15 +1,33 @@
1
+ /** 脚本绝对路径(界面上要写给用户复制的命令用;别在别处再拼一份)。 */
2
+ export declare function scriptPath(): string;
3
+ /**
4
+ * 脚本失败的统一出口:**没身份单独成类**(→ `LEDGER_IDENTITY_MISSING`),其余照旧是普通 Error。
5
+ * 各处别自己 `new Error(result.error)` —— 那样「没身份」就淹进「台账坏了」里,界面分不出来。
6
+ */
7
+ export declare function failureOf(result: {
8
+ readonly error: string;
9
+ readonly identityMissing?: boolean;
10
+ }, prefix?: string, workspaceId?: string | null): Error;
1
11
  export type LedgerScriptResult<T> = {
2
12
  ok: true;
3
13
  data: T;
4
14
  } | {
5
15
  ok: false;
6
16
  error: string;
17
+ /** 脚本退出码 4 = 这台机器还没有身份(`env.Current` 的专用码,文案不变)。 */
18
+ identityMissing?: boolean;
7
19
  };
8
20
  export interface RunOptions {
9
21
  /** 测试用:覆盖脚本路径。 */
10
22
  scriptPath?: string;
11
23
  /** 覆盖超时。 */
12
24
  timeoutMs?: number;
25
+ /**
26
+ * 过程步骤回调(写命令专用):skill 每进一个阶段就在 stderr 打一行 `#step <key>`
27
+ * (`shell/render.go` 的 stepf,只有宿主设了 `TASK_LEDGER_STEPS=1` 才打)。
28
+ * 给了它才让 skill 打标记,读命令不传、不产生任何额外输出。
29
+ */
30
+ onStep?: (step: string) => void;
13
31
  }
14
32
  /**
15
33
  * 读命令:跑内嵌台账程序,收 stdout JSON。
@@ -21,3 +39,14 @@ export interface RunOptions {
21
39
  * @param opts.timeoutMs - 覆盖超时
22
40
  */
23
41
  export declare function runLedgerScript<T = unknown>(sub: string, args: readonly string[], opts?: RunOptions): Promise<LedgerScriptResult<T>>;
42
+ /** 写命令结果:写动作只回人话(`shell/render.go` 的 RenderResult),**不认 --json** ⇒ 不解析 stdout。 */
43
+ export type LedgerCommandResult = {
44
+ readonly ok: true;
45
+ readonly output: string;
46
+ } | {
47
+ readonly ok: false;
48
+ readonly error: string;
49
+ readonly identityMissing?: boolean;
50
+ };
51
+ /** 写命令:认领 / 释放 / 结案这类改台账的动作走它(读命令走 `runLedgerScript`)。 */
52
+ export declare function runLedgerCommand(sub: string, args: readonly string[], opts?: RunOptions): Promise<LedgerCommandResult>;