trove-cli 0.0.0-stage → 0.3.2

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 (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -2
  3. package/dist/adapters/file-source.d.ts +31 -0
  4. package/dist/adapters/file-source.js +97 -0
  5. package/dist/adapters/file-source.js.map +1 -0
  6. package/dist/adapters/index.d.ts +34 -0
  7. package/dist/adapters/index.js +146 -0
  8. package/dist/adapters/index.js.map +1 -0
  9. package/dist/adapters/opencode.d.ts +30 -0
  10. package/dist/adapters/opencode.js +135 -0
  11. package/dist/adapters/opencode.js.map +1 -0
  12. package/dist/cli.d.ts +58 -0
  13. package/dist/cli.js +974 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/config.d.ts +81 -0
  16. package/dist/config.js +99 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/core/archive.d.ts +27 -0
  19. package/dist/core/archive.js +101 -0
  20. package/dist/core/archive.js.map +1 -0
  21. package/dist/core/chunker.d.ts +28 -0
  22. package/dist/core/chunker.js +67 -0
  23. package/dist/core/chunker.js.map +1 -0
  24. package/dist/core/clean.d.ts +36 -0
  25. package/dist/core/clean.js +182 -0
  26. package/dist/core/clean.js.map +1 -0
  27. package/dist/core/collect.d.ts +39 -0
  28. package/dist/core/collect.js +194 -0
  29. package/dist/core/collect.js.map +1 -0
  30. package/dist/core/concurrency.d.ts +2 -0
  31. package/dist/core/concurrency.js +16 -0
  32. package/dist/core/concurrency.js.map +1 -0
  33. package/dist/core/cursor.d.ts +31 -0
  34. package/dist/core/cursor.js +31 -0
  35. package/dist/core/cursor.js.map +1 -0
  36. package/dist/core/events/claude.d.ts +7 -0
  37. package/dist/core/events/claude.js +287 -0
  38. package/dist/core/events/claude.js.map +1 -0
  39. package/dist/core/events/codex.d.ts +6 -0
  40. package/dist/core/events/codex.js +149 -0
  41. package/dist/core/events/codex.js.map +1 -0
  42. package/dist/core/events/doubao.d.ts +9 -0
  43. package/dist/core/events/doubao.js +151 -0
  44. package/dist/core/events/doubao.js.map +1 -0
  45. package/dist/core/events/index.d.ts +6 -0
  46. package/dist/core/events/index.js +20 -0
  47. package/dist/core/events/index.js.map +1 -0
  48. package/dist/core/events/opencode.d.ts +7 -0
  49. package/dist/core/events/opencode.js +189 -0
  50. package/dist/core/events/opencode.js.map +1 -0
  51. package/dist/core/events/schema.d.ts +83 -0
  52. package/dist/core/events/schema.js +35 -0
  53. package/dist/core/events/schema.js.map +1 -0
  54. package/dist/core/layout.d.ts +26 -0
  55. package/dist/core/layout.js +122 -0
  56. package/dist/core/layout.js.map +1 -0
  57. package/dist/core/lessons/analyze.d.ts +40 -0
  58. package/dist/core/lessons/analyze.js +107 -0
  59. package/dist/core/lessons/analyze.js.map +1 -0
  60. package/dist/core/lessons/audit.d.ts +51 -0
  61. package/dist/core/lessons/audit.js +57 -0
  62. package/dist/core/lessons/audit.js.map +1 -0
  63. package/dist/core/lessons/compaction.d.ts +5 -0
  64. package/dist/core/lessons/compaction.js +58 -0
  65. package/dist/core/lessons/compaction.js.map +1 -0
  66. package/dist/core/lessons/config.d.ts +15 -0
  67. package/dist/core/lessons/config.js +31 -0
  68. package/dist/core/lessons/config.js.map +1 -0
  69. package/dist/core/lessons/existing.d.ts +4 -0
  70. package/dist/core/lessons/existing.js +22 -0
  71. package/dist/core/lessons/existing.js.map +1 -0
  72. package/dist/core/lessons/extract.d.ts +21 -0
  73. package/dist/core/lessons/extract.js +106 -0
  74. package/dist/core/lessons/extract.js.map +1 -0
  75. package/dist/core/lessons/frontmatter.d.ts +29 -0
  76. package/dist/core/lessons/frontmatter.js +87 -0
  77. package/dist/core/lessons/frontmatter.js.map +1 -0
  78. package/dist/core/lessons/graph.d.ts +256 -0
  79. package/dist/core/lessons/graph.js +86 -0
  80. package/dist/core/lessons/graph.js.map +1 -0
  81. package/dist/core/lessons/jev.d.ts +36 -0
  82. package/dist/core/lessons/jev.js +100 -0
  83. package/dist/core/lessons/jev.js.map +1 -0
  84. package/dist/core/lessons/layout.d.ts +2 -0
  85. package/dist/core/lessons/layout.js +56 -0
  86. package/dist/core/lessons/layout.js.map +1 -0
  87. package/dist/core/lessons/learn.d.ts +38 -0
  88. package/dist/core/lessons/learn.js +190 -0
  89. package/dist/core/lessons/learn.js.map +1 -0
  90. package/dist/core/lessons/llm.d.ts +42 -0
  91. package/dist/core/lessons/llm.js +106 -0
  92. package/dist/core/lessons/llm.js.map +1 -0
  93. package/dist/core/lessons/memo.d.ts +19 -0
  94. package/dist/core/lessons/memo.js +170 -0
  95. package/dist/core/lessons/memo.js.map +1 -0
  96. package/dist/core/lessons/merge.d.ts +39 -0
  97. package/dist/core/lessons/merge.js +79 -0
  98. package/dist/core/lessons/merge.js.map +1 -0
  99. package/dist/core/lessons/queue.d.ts +6 -0
  100. package/dist/core/lessons/queue.js +52 -0
  101. package/dist/core/lessons/queue.js.map +1 -0
  102. package/dist/core/lessons/recent.d.ts +21 -0
  103. package/dist/core/lessons/recent.js +111 -0
  104. package/dist/core/lessons/recent.js.map +1 -0
  105. package/dist/core/lessons/schema.d.ts +26 -0
  106. package/dist/core/lessons/schema.js +64 -0
  107. package/dist/core/lessons/schema.js.map +1 -0
  108. package/dist/core/lessons/select.d.ts +19 -0
  109. package/dist/core/lessons/select.js +55 -0
  110. package/dist/core/lessons/select.js.map +1 -0
  111. package/dist/core/lessons/sidecar.d.ts +23 -0
  112. package/dist/core/lessons/sidecar.js +171 -0
  113. package/dist/core/lessons/sidecar.js.map +1 -0
  114. package/dist/core/lessons/storage.d.ts +21 -0
  115. package/dist/core/lessons/storage.js +65 -0
  116. package/dist/core/lessons/storage.js.map +1 -0
  117. package/dist/core/lessons/usage.d.ts +31 -0
  118. package/dist/core/lessons/usage.js +91 -0
  119. package/dist/core/lessons/usage.js.map +1 -0
  120. package/dist/core/lessons/write.d.ts +18 -0
  121. package/dist/core/lessons/write.js +131 -0
  122. package/dist/core/lessons/write.js.map +1 -0
  123. package/dist/core/library/build.d.ts +22 -0
  124. package/dist/core/library/build.js +163 -0
  125. package/dist/core/library/build.js.map +1 -0
  126. package/dist/core/library/group.d.ts +14 -0
  127. package/dist/core/library/group.js +129 -0
  128. package/dist/core/library/group.js.map +1 -0
  129. package/dist/core/library/index-file.d.ts +7 -0
  130. package/dist/core/library/index-file.js +250 -0
  131. package/dist/core/library/index-file.js.map +1 -0
  132. package/dist/core/library/schema.d.ts +78 -0
  133. package/dist/core/library/schema.js +37 -0
  134. package/dist/core/library/schema.js.map +1 -0
  135. package/dist/core/library/skills.d.ts +26 -0
  136. package/dist/core/library/skills.js +168 -0
  137. package/dist/core/library/skills.js.map +1 -0
  138. package/dist/core/library/sync.d.ts +48 -0
  139. package/dist/core/library/sync.js +271 -0
  140. package/dist/core/library/sync.js.map +1 -0
  141. package/dist/core/lock.d.ts +6 -0
  142. package/dist/core/lock.js +56 -0
  143. package/dist/core/lock.js.map +1 -0
  144. package/dist/core/pull.d.ts +23 -0
  145. package/dist/core/pull.js +147 -0
  146. package/dist/core/pull.js.map +1 -0
  147. package/dist/core/push.d.ts +29 -0
  148. package/dist/core/push.js +170 -0
  149. package/dist/core/push.js.map +1 -0
  150. package/dist/core/remote-layout.d.ts +67 -0
  151. package/dist/core/remote-layout.js +94 -0
  152. package/dist/core/remote-layout.js.map +1 -0
  153. package/dist/core/sessions.d.ts +29 -0
  154. package/dist/core/sessions.js +96 -0
  155. package/dist/core/sessions.js.map +1 -0
  156. package/dist/core/state.d.ts +94 -0
  157. package/dist/core/state.js +62 -0
  158. package/dist/core/state.js.map +1 -0
  159. package/dist/core/sync.d.ts +48 -0
  160. package/dist/core/sync.js +140 -0
  161. package/dist/core/sync.js.map +1 -0
  162. package/dist/core/time-window.d.ts +8 -0
  163. package/dist/core/time-window.js +36 -0
  164. package/dist/core/time-window.js.map +1 -0
  165. package/dist/daemon.d.ts +73 -0
  166. package/dist/daemon.js +272 -0
  167. package/dist/daemon.js.map +1 -0
  168. package/dist/format.d.ts +4 -0
  169. package/dist/format.js +28 -0
  170. package/dist/format.js.map +1 -0
  171. package/dist/main.d.ts +2 -0
  172. package/dist/main.js +9 -0
  173. package/dist/main.js.map +1 -0
  174. package/dist/paths.d.ts +19 -0
  175. package/dist/paths.js +19 -0
  176. package/dist/paths.js.map +1 -0
  177. package/dist/progress-console.d.ts +12 -0
  178. package/dist/progress-console.js +42 -0
  179. package/dist/progress-console.js.map +1 -0
  180. package/dist/progress.d.ts +7 -0
  181. package/dist/progress.js +6 -0
  182. package/dist/progress.js.map +1 -0
  183. package/dist/remote/create-remote.d.ts +20 -0
  184. package/dist/remote/create-remote.js +86 -0
  185. package/dist/remote/create-remote.js.map +1 -0
  186. package/dist/remote/prompt.d.ts +17 -0
  187. package/dist/remote/prompt.js +111 -0
  188. package/dist/remote/prompt.js.map +1 -0
  189. package/dist/remote/s3-sigv4.d.ts +28 -0
  190. package/dist/remote/s3-sigv4.js +100 -0
  191. package/dist/remote/s3-sigv4.js.map +1 -0
  192. package/dist/remote/s3-store.d.ts +75 -0
  193. package/dist/remote/s3-store.js +438 -0
  194. package/dist/remote/s3-store.js.map +1 -0
  195. package/dist/remote/s3-vendors.d.ts +31 -0
  196. package/dist/remote/s3-vendors.js +47 -0
  197. package/dist/remote/s3-vendors.js.map +1 -0
  198. package/dist/remote/store.d.ts +44 -0
  199. package/dist/remote/store.js +2 -0
  200. package/dist/remote/store.js.map +1 -0
  201. package/dist/remote/webdav-store.d.ts +41 -0
  202. package/dist/remote/webdav-store.js +285 -0
  203. package/dist/remote/webdav-store.js.map +1 -0
  204. package/dist/sqlite.d.ts +3 -0
  205. package/dist/sqlite.js +12 -0
  206. package/dist/sqlite.js.map +1 -0
  207. package/dist/status.d.ts +82 -0
  208. package/dist/status.js +162 -0
  209. package/dist/status.js.map +1 -0
  210. package/package.json +43 -4
@@ -0,0 +1,78 @@
1
+ import type { HarnessId } from '../../adapters/index.js';
2
+ /** library 里的会话变体:同一 (harness, session) 来自一台设备的一份事件流。 */
3
+ export interface SessionVariant {
4
+ device: string;
5
+ /** 相对 library 根: sessions/<harness>/<session>/<device>.jsonl */
6
+ path: string;
7
+ hash: string;
8
+ events: number;
9
+ }
10
+ /** 统一索引里的会话条目(合并后的视图)。 */
11
+ export interface MergedSession {
12
+ harness: HarnessId;
13
+ session: string;
14
+ title?: string;
15
+ firstText?: string;
16
+ cwd?: string;
17
+ model?: string;
18
+ startedAt?: string;
19
+ canonical: SessionVariant;
20
+ variants: SessionVariant[];
21
+ /** 参与的设备 */
22
+ sources: string[];
23
+ }
24
+ /** 每设备发布的索引分片。 */
25
+ export interface IndexShard {
26
+ v: 1;
27
+ device: string;
28
+ mapper: string;
29
+ updatedAt: string;
30
+ sessions: Record<string, SessionVariant & {
31
+ title?: string;
32
+ firstText?: string;
33
+ cwd?: string;
34
+ model?: string;
35
+ startedAt?: string;
36
+ rawSources: string[];
37
+ }>;
38
+ skills: Record<string, SkillEntry>;
39
+ }
40
+ export type SkillSource = 'self' | 'github' | 'skillsh' | 'unknown';
41
+ export interface SkillFile {
42
+ /** 相对 skill 版本目录 */
43
+ rel: string;
44
+ size: number;
45
+ }
46
+ export interface SkillEntry {
47
+ name: string;
48
+ hash: string;
49
+ version?: string;
50
+ source: SkillSource;
51
+ origin?: string;
52
+ updatedAt?: string;
53
+ /** 版本目录内的文件清单: versions/<name>/<hash>/<rel> */
54
+ files: SkillFile[];
55
+ }
56
+ export interface MergedSkill {
57
+ name: string;
58
+ latest: string;
59
+ versions: Array<SkillEntry & {
60
+ devices: string[];
61
+ }>;
62
+ }
63
+ export interface LibraryIndex {
64
+ v: 1;
65
+ generatedAt: string;
66
+ hash: string;
67
+ sessions: Record<string, MergedSession>;
68
+ skills: Record<string, MergedSkill>;
69
+ }
70
+ export declare function sessionKeyOf(harness: string, session: string): string;
71
+ /** 技能版本比较:能解析 semver 的优先,semver 大的更新;都不能解析返回 0。 */
72
+ export declare function compareVersions(a?: string, b?: string): number;
73
+ /** 从候选版本里选“最新”:semver → updatedAt → hash,保证确定性。 */
74
+ export declare function pickLatestHash(versions: Array<{
75
+ hash: string;
76
+ version?: string;
77
+ updatedAt?: string;
78
+ }>): string;
@@ -0,0 +1,37 @@
1
+ export function sessionKeyOf(harness, session) {
2
+ return `${harness}:${session}`;
3
+ }
4
+ /** 技能版本比较:能解析 semver 的优先,semver 大的更新;都不能解析返回 0。 */
5
+ export function compareVersions(a, b) {
6
+ const parse = (v) => {
7
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v ?? '');
8
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
9
+ };
10
+ const pa = parse(a);
11
+ const pb = parse(b);
12
+ if (pa && pb) {
13
+ for (let i = 0; i < 3; i++)
14
+ if (pa[i] !== pb[i])
15
+ return pa[i] - pb[i];
16
+ return 0;
17
+ }
18
+ if (pa)
19
+ return 1;
20
+ if (pb)
21
+ return -1;
22
+ return 0;
23
+ }
24
+ /** 从候选版本里选“最新”:semver → updatedAt → hash,保证确定性。 */
25
+ export function pickLatestHash(versions) {
26
+ const sorted = [...versions].sort((a, b) => {
27
+ const v = compareVersions(b.version, a.version);
28
+ if (v !== 0)
29
+ return v;
30
+ const t = (b.updatedAt ?? '').localeCompare(a.updatedAt ?? '');
31
+ if (t !== 0)
32
+ return t;
33
+ return a.hash.localeCompare(b.hash);
34
+ });
35
+ return sorted[0].hash;
36
+ }
37
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/core/library/schema.ts"],"names":[],"mappings":"AAqEA,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,OAAe;IAC3D,OAAO,GAAG,OAAO,IAAI,OAAO,EAAE,CAAC;AACjC,CAAC;AAGD,mDAAmD;AACnD,MAAM,UAAU,eAAe,CAAC,CAAU,EAAE,CAAU;IACpD,MAAM,KAAK,GAAG,CAAC,CAAU,EAAmC,EAAE;QAC5D,MAAM,CAAC,GAAG,wBAAwB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACjD,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC/D,CAAC,CAAC;IACF,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACpB,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACpB,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;QACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;YAAE,IAAI,EAAE,CAAC,CAAC,CAAE,KAAK,EAAE,CAAC,CAAC,CAAE;gBAAE,OAAO,EAAE,CAAC,CAAC,CAAE,GAAG,EAAE,CAAC,CAAC,CAAE,CAAC;QAC1E,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,EAAE;QAAE,OAAO,CAAC,CAAC;IACjB,IAAI,EAAE;QAAE,OAAO,CAAC,CAAC,CAAC;IAClB,OAAO,CAAC,CAAC;AACX,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,cAAc,CAC5B,QAAuE;IAEvE,MAAM,MAAM,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACzC,MAAM,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;QAChD,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;QACtB,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;QAC/D,IAAI,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,CAAC;QACtB,OAAO,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,CAAC,CAAE,CAAC,IAAI,CAAC;AACzB,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { State } from '../state.js';
2
+ import { type SkillEntry, type SkillSource } from './schema.js';
3
+ /** 解析 SKILL.md 的简单 frontmatter(不引入 YAML 依赖)。 */
4
+ export declare function parseFrontmatter(text: string): Record<string, string>;
5
+ /** 从 .git/config 里取 origin URL。 */
6
+ export declare function parseOrigin(gitConfig: string): string | undefined;
7
+ export declare function classifySource(origin?: string, declared?: string): SkillSource;
8
+ export interface BuildSkillsOptions {
9
+ vaultRoot: string;
10
+ deviceId: string;
11
+ state: State;
12
+ now?: () => number;
13
+ }
14
+ export interface BuildSkillsResult {
15
+ /** 每个技能的最新版本(供索引分片展示) */
16
+ entries: Record<string, SkillEntry>;
17
+ versions: number;
18
+ written: number;
19
+ }
20
+ /**
21
+ * 从本设备 raw 里的 `skills/**` 汇总技能:
22
+ * library/skills/<name>/versions/<hash>/… 内容寻址,保留各版本
23
+ * library/skills/<name>/meta/<device>.json 本设备看到的版本与来源
24
+ * 同名按内容 hash 去重,来源由 frontmatter / .git/config 判定。
25
+ */
26
+ export declare function buildSkills(opts: BuildSkillsOptions): Promise<BuildSkillsResult>;
@@ -0,0 +1,168 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { librarySkillsDir, rawDeviceDir, safeSessionName } from '../layout.js';
5
+ import { pickLatestHash } from './schema.js';
6
+ const SKILL_HARNESSES = ['claude-code', 'codex'];
7
+ const SKIP_DIRS = new Set(['.git', 'node_modules']);
8
+ async function walk(root, rel = '') {
9
+ let entries;
10
+ try {
11
+ entries = await readdir(rel ? join(root, rel) : root, { withFileTypes: true });
12
+ }
13
+ catch {
14
+ return [];
15
+ }
16
+ const out = [];
17
+ for (const e of entries) {
18
+ const p = rel ? `${rel}/${e.name}` : e.name;
19
+ if (e.isDirectory()) {
20
+ if (SKIP_DIRS.has(e.name))
21
+ continue;
22
+ out.push(...(await walk(root, p)));
23
+ }
24
+ else if (e.isFile()) {
25
+ if (e.name === '.DS_Store')
26
+ continue;
27
+ out.push(p);
28
+ }
29
+ }
30
+ return out;
31
+ }
32
+ /** 解析 SKILL.md 的简单 frontmatter(不引入 YAML 依赖)。 */
33
+ export function parseFrontmatter(text) {
34
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
35
+ if (!m)
36
+ return {};
37
+ const out = {};
38
+ for (const line of m[1].split(/\r?\n/)) {
39
+ if (!line.trim() || line.trimStart().startsWith('#'))
40
+ continue;
41
+ const i = line.indexOf(':');
42
+ if (i < 0)
43
+ continue;
44
+ const key = line.slice(0, i).trim();
45
+ const value = line.slice(i + 1).trim().replace(/^["']|["']$/g, '');
46
+ if (key)
47
+ out[key] = value;
48
+ }
49
+ return out;
50
+ }
51
+ /** 从 .git/config 里取 origin URL。 */
52
+ export function parseOrigin(gitConfig) {
53
+ const m = /\[remote "origin"\][\s\S]*?url\s*=\s*(.+)/.exec(gitConfig);
54
+ return m?.[1]?.trim();
55
+ }
56
+ export function classifySource(origin, declared) {
57
+ if (declared === 'github' || declared === 'skillsh' || declared === 'self')
58
+ return declared;
59
+ if (!origin)
60
+ return 'self';
61
+ if (/github\.com/i.test(origin))
62
+ return 'github';
63
+ if (/skills?\.sh/i.test(origin))
64
+ return 'skillsh';
65
+ return 'unknown';
66
+ }
67
+ function contentHash(files) {
68
+ const hash = createHash('sha256');
69
+ for (const f of files) {
70
+ hash.update(f.rel).update('\0').update(f.bytes).update('\0');
71
+ }
72
+ return hash.digest('hex');
73
+ }
74
+ async function writeAtomic(path, text) {
75
+ await mkdir(dirname(path), { recursive: true });
76
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
77
+ await writeFile(tmp, text, 'utf8');
78
+ await rename(tmp, path);
79
+ }
80
+ /**
81
+ * 从本设备 raw 里的 `skills/**` 汇总技能:
82
+ * library/skills/<name>/versions/<hash>/… 内容寻址,保留各版本
83
+ * library/skills/<name>/meta/<device>.json 本设备看到的版本与来源
84
+ * 同名按内容 hash 去重,来源由 frontmatter / .git/config 判定。
85
+ */
86
+ export async function buildSkills(opts) {
87
+ const { vaultRoot, deviceId, state } = opts;
88
+ const now = opts.now ?? Date.now;
89
+ const byName = new Map();
90
+ let written = 0;
91
+ for (const harness of SKILL_HARNESSES) {
92
+ const root = join(rawDeviceDir(vaultRoot, deviceId), harness, 'skills');
93
+ let dirs;
94
+ try {
95
+ dirs = (await readdir(root, { withFileTypes: true })).filter((e) => e.isDirectory());
96
+ }
97
+ catch {
98
+ continue;
99
+ }
100
+ for (const dir of dirs) {
101
+ const skillRoot = join(root, dir.name);
102
+ const relFiles = (await walk(skillRoot)).sort();
103
+ if (!relFiles.length)
104
+ continue;
105
+ const files = [];
106
+ for (const rel of relFiles) {
107
+ files.push({ rel, bytes: await readFile(join(skillRoot, rel)) });
108
+ }
109
+ const hash = contentHash(files);
110
+ const skillMd = files.find((f) => /(^|\/)SKILL\.md$/i.test(f.rel));
111
+ const fm = skillMd ? parseFrontmatter(skillMd.bytes.toString('utf8')) : {};
112
+ // .git 不进入版本目录,但来源判定需要读它的 origin
113
+ let origin;
114
+ try {
115
+ origin = parseOrigin(await readFile(join(skillRoot, '.git/config'), 'utf8'));
116
+ }
117
+ catch {
118
+ origin = undefined;
119
+ }
120
+ const name = fm['name'] || dir.name;
121
+ const source = classifySource(origin, fm['source']);
122
+ const version = fm['version'] || undefined;
123
+ const mtimes = relFiles
124
+ .map((rel) => state.sources[harness]?.[`skills/${dir.name}/${rel}`]?.mtimeMs)
125
+ .filter((v) => typeof v === 'number');
126
+ const updatedAt = new Date(mtimes.length ? Math.max(...mtimes) : now()).toISOString();
127
+ const entry = {
128
+ name,
129
+ hash,
130
+ ...(version ? { version } : {}),
131
+ source,
132
+ ...(origin ? { origin } : {}),
133
+ updatedAt,
134
+ files: files.map((f) => ({ rel: f.rel, size: f.bytes.length })),
135
+ };
136
+ const safe = safeSessionName(name);
137
+ const versionDir = join(librarySkillsDir(vaultRoot), safe, 'versions', hash);
138
+ try {
139
+ await stat(versionDir);
140
+ }
141
+ catch {
142
+ for (const f of files) {
143
+ const target = join(versionDir, ...f.rel.split('/'));
144
+ await mkdir(dirname(target), { recursive: true });
145
+ await writeFile(target, f.bytes);
146
+ }
147
+ written++;
148
+ }
149
+ const versions = byName.get(name) ?? new Map();
150
+ versions.set(hash, entry);
151
+ byName.set(name, versions);
152
+ }
153
+ }
154
+ const entries = {};
155
+ for (const [name, versions] of [...byName.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
156
+ const list = [...versions.values()];
157
+ const latest = pickLatestHash(list);
158
+ entries[name] = versions.get(latest);
159
+ const meta = {
160
+ name,
161
+ device: deviceId,
162
+ versions: list.sort((a, b) => a.hash.localeCompare(b.hash)),
163
+ };
164
+ await writeAtomic(join(librarySkillsDir(vaultRoot), safeSessionName(name), 'meta', `${deviceId}.json`), JSON.stringify(meta, null, 2) + '\n');
165
+ }
166
+ return { entries, versions: Object.keys(entries).length, written };
167
+ }
168
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.js","sourceRoot":"","sources":["../../../src/core/library/skills.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import { type Progress } from '../../progress.js';
2
+ import type { RemoteStore } from '../../remote/store.js';
3
+ import type { State } from '../state.js';
4
+ export interface PushLibraryOptions {
5
+ remote: RemoteStore;
6
+ vaultRoot: string;
7
+ deviceId: string;
8
+ state: State;
9
+ /** 当前远端身份;变了说明推送水位属于旧后端,必须整份重推。 */
10
+ remoteId?: string;
11
+ /** 用于周期性落盘进度:中断后已完成文件不会被重推 */
12
+ statePath: string;
13
+ progress?: Progress;
14
+ signal?: AbortSignal;
15
+ }
16
+ export interface PushLibraryResult {
17
+ uploaded: number;
18
+ bytes: number;
19
+ skipped: number;
20
+ }
21
+ /**
22
+ * 推送 library 里本设备负责的文件:
23
+ * sessions/**、skills/**、lessons/**、index/<device>.json
24
+ * 每台设备只写自己的分片;会话文件按大小+mtime 判断是否变化。
25
+ * lessons 是共享沉淀,没有设备分片,多设备会互相覆盖(最后写赢)。
26
+ */
27
+ /** library 里需要推送的文件(相对 library 根);status 的待同步计数复用同一口径。 */
28
+ export declare function listLibraryFiles(vaultRoot: string): Promise<string[]>;
29
+ export declare function pushLibrary(opts: PushLibraryOptions): Promise<PushLibraryResult>;
30
+ export interface PullLibraryOptions {
31
+ remote: RemoteStore;
32
+ vaultRoot: string;
33
+ deviceId: string;
34
+ state: State;
35
+ progress?: Progress;
36
+ signal?: AbortSignal;
37
+ }
38
+ export interface PullLibraryResult {
39
+ downloaded: number;
40
+ bytes: number;
41
+ skipped: number;
42
+ }
43
+ /**
44
+ * 拉取所有设备的 library 分片与文件:
45
+ * index/*.json → 会话文件、技能版本
46
+ * 已存在且 hash 一致的文件不重复下载;下载后校验 sha256。
47
+ */
48
+ export declare function pullLibrary(opts: PullLibraryOptions): Promise<PullLibraryResult>;
@@ -0,0 +1,271 @@
1
+ import { mkdir, readdir, rename, stat, unlink, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { hashFile } from '../chunker.js';
4
+ import { throwIfAborted } from '../collect.js';
5
+ import { libraryDir, safeSessionName } from '../layout.js';
6
+ import { silentProgress } from '../../progress.js';
7
+ import { rebuildUnifiedIndex } from './index-file.js';
8
+ import { saveState } from '../state.js';
9
+ async function walk(root, rel = '') {
10
+ let entries;
11
+ try {
12
+ entries = await readdir(rel ? join(root, rel) : root, { withFileTypes: true });
13
+ }
14
+ catch {
15
+ return [];
16
+ }
17
+ const out = [];
18
+ for (const e of entries) {
19
+ const p = rel ? `${rel}/${e.name}` : e.name;
20
+ if (e.isDirectory())
21
+ out.push(...(await walk(root, p)));
22
+ else if (e.isFile() && !e.name.includes('.tmp-'))
23
+ out.push(p);
24
+ }
25
+ return out;
26
+ }
27
+ async function writeAtomic(path, data) {
28
+ await mkdir(dirname(path), { recursive: true });
29
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
30
+ await writeFile(tmp, data);
31
+ await rename(tmp, path);
32
+ }
33
+ /** 检查点间隔(ms):中断最多损失一分钟的已推记录。 */
34
+ const CHECKPOINT_MS = 60_000;
35
+ /**
36
+ * 推送 library 里本设备负责的文件:
37
+ * sessions/**、skills/**、lessons/**、index/<device>.json
38
+ * 每台设备只写自己的分片;会话文件按大小+mtime 判断是否变化。
39
+ * lessons 是共享沉淀,没有设备分片,多设备会互相覆盖(最后写赢)。
40
+ */
41
+ /** library 里需要推送的文件(相对 library 根);status 的待同步计数复用同一口径。 */
42
+ export async function listLibraryFiles(vaultRoot) {
43
+ const root = libraryDir(vaultRoot);
44
+ const rank = (rel) => (rel.startsWith('index/') ? 1 : 0);
45
+ return [
46
+ ...(await walk(root, 'sessions')),
47
+ ...(await walk(root, 'skills')),
48
+ ...(await walk(root, 'lessons')),
49
+ ...(await walk(root, 'index')).filter((rel) => rel.endsWith('.json')),
50
+ // index 分片是引用其他文件的提交点:必须最后传,否则中断会留下指向不存在文件的悬空引用
51
+ ].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
52
+ }
53
+ export async function pushLibrary(opts) {
54
+ const { remote, vaultRoot, state } = opts;
55
+ const progress = opts.progress ?? silentProgress;
56
+ const root = libraryDir(vaultRoot);
57
+ const files = await listLibraryFiles(vaultRoot);
58
+ let uploaded = 0;
59
+ let bytes = 0;
60
+ let skipped = 0;
61
+ let n = 0;
62
+ let lastCheckpoint = Date.now();
63
+ // 换后端时旧后端的推送记账对新远端无效:就地清空并立刻落盘。
64
+ // 这样即使进程中途被杀,下次也只会补传未完成的文件,而不是把整份 library 重推。
65
+ if (opts.remoteId && state.library.remoteId !== opts.remoteId) {
66
+ state.library.pushed = {};
67
+ state.library.remoteId = opts.remoteId;
68
+ await saveState(opts.statePath, state);
69
+ }
70
+ const sameRemote = !opts.remoteId || state.library.remoteId === opts.remoteId;
71
+ // 中断(abort/报错)时保住已完成文件的记账:重启后按 state 跳过,不整轮重推
72
+ const checkpoint = async () => {
73
+ await saveState(opts.statePath, state);
74
+ lastCheckpoint = Date.now();
75
+ };
76
+ try {
77
+ for (const rel of files) {
78
+ throwIfAborted(opts.signal);
79
+ n++;
80
+ const abs = join(root, ...rel.split('/'));
81
+ const st = await stat(abs);
82
+ const prev = state.library.pushed[rel];
83
+ if (sameRemote && prev && prev.size === st.size && prev.mtimeMs === Math.round(st.mtimeMs)) {
84
+ skipped++;
85
+ continue;
86
+ }
87
+ await remote.writeFile(`library/${rel}`, abs);
88
+ state.library.pushed[rel] = { size: st.size, mtimeMs: Math.round(st.mtimeMs) };
89
+ uploaded++;
90
+ bytes += st.size;
91
+ progress.item(rel, n, files.length, st.size);
92
+ if (Date.now() - lastCheckpoint >= CHECKPOINT_MS)
93
+ await checkpoint();
94
+ }
95
+ }
96
+ finally {
97
+ if (uploaded > 0)
98
+ await checkpoint();
99
+ }
100
+ return { uploaded, bytes, skipped };
101
+ }
102
+ async function localHash(path) {
103
+ try {
104
+ return await hashFile(path);
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ /** 远端 404:索引引用的文件不存在(分片悬空),可跳过而不是中断整轮同步。 */
111
+ function isNotFound(err) {
112
+ const e = err;
113
+ return (e?.status ?? e?.statusCode ?? e?.response?.status) === 404;
114
+ }
115
+ /** 递归列远端目录下的文件(RemoteStore.list 只列一层)。 */
116
+ async function walkRemote(remote, dir) {
117
+ const out = [];
118
+ for (const e of await remote.list(dir)) {
119
+ const p = `${dir}/${e.name}`;
120
+ if (e.type === 'directory')
121
+ out.push(...(await walkRemote(remote, p)));
122
+ else
123
+ out.push(p);
124
+ }
125
+ return out;
126
+ }
127
+ /**
128
+ * 拉取所有设备的 library 分片与文件:
129
+ * index/*.json → 会话文件、技能版本
130
+ * 已存在且 hash 一致的文件不重复下载;下载后校验 sha256。
131
+ */
132
+ export async function pullLibrary(opts) {
133
+ const { remote, vaultRoot, state } = opts;
134
+ const progress = opts.progress ?? silentProgress;
135
+ const root = libraryDir(vaultRoot);
136
+ let downloaded = 0;
137
+ let bytes = 0;
138
+ let skipped = 0;
139
+ const shards = (await remote.list('library/index')).filter((e) => e.type === 'file');
140
+ progress.phase('library', `${shards.length} 个设备分片`);
141
+ let itemNo = 0;
142
+ for (const shardEntry of shards) {
143
+ throwIfAborted(opts.signal);
144
+ const shardPath = `library/index/${shardEntry.name}`;
145
+ const raw = await remote.readJson(shardPath);
146
+ if (!raw || typeof raw !== 'object')
147
+ continue;
148
+ const shard = raw;
149
+ if (shard.v !== 1 || !shard.sessions)
150
+ continue;
151
+ const device = shard.device;
152
+ const isSelf = device === opts.deviceId;
153
+ // 分片本身也要落地,统一索引才能离线重建;本机分片是权威,只在缺失时恢复
154
+ const shardAbs = join(root, 'index', shardEntry.name);
155
+ if (!isSelf || (await localHash(shardAbs)) === null) {
156
+ await writeAtomic(shardAbs, JSON.stringify(shard, null, 2) + '\n');
157
+ }
158
+ const pulled = (state.library.pulled[device] ??= {});
159
+ for (const entry of Object.values(shard.sessions)) {
160
+ throwIfAborted(opts.signal);
161
+ itemNo++;
162
+ const remotePath = `library/${entry.path}`;
163
+ const abs = join(root, ...entry.path.split('/'));
164
+ // 本机文件是权威:存在就不拿远端(可能是旧分片)覆盖,只在缺失时恢复
165
+ if (isSelf && (await localHash(abs)) !== null) {
166
+ skipped++;
167
+ continue;
168
+ }
169
+ if (pulled[entry.path] === entry.hash) {
170
+ skipped++;
171
+ continue;
172
+ }
173
+ if ((await localHash(abs)) === entry.hash) {
174
+ pulled[entry.path] = entry.hash;
175
+ skipped++;
176
+ continue;
177
+ }
178
+ const tmp = `${abs}.tmp-${process.pid}-${Date.now()}`;
179
+ try {
180
+ await mkdir(dirname(abs), { recursive: true });
181
+ await remote.readFile(remotePath, tmp);
182
+ const got = await hashFile(tmp);
183
+ if (got !== entry.hash)
184
+ throw new Error(`library 文件校验失败: ${entry.path}`);
185
+ await rename(tmp, abs);
186
+ }
187
+ catch (err) {
188
+ await unlink(tmp).catch(() => { });
189
+ // 分片可能来自已离线设备或旧数据,引用了不存在的对象:跳过,不影响其他文件
190
+ if (isNotFound(err)) {
191
+ console.warn(`[trove] 远端缺少索引引用的文件,已跳过: ${entry.path}`);
192
+ continue;
193
+ }
194
+ throw err;
195
+ }
196
+ pulled[entry.path] = entry.hash;
197
+ downloaded++;
198
+ bytes += (await stat(abs)).size;
199
+ progress.item(entry.path, itemNo, shards.length * 100, bytes);
200
+ }
201
+ // 技能:meta 与版本文件
202
+ for (const name of Object.keys(shard.skills ?? {})) {
203
+ const safe = safeSessionName(name);
204
+ const metaFiles = (await remote.list(`library/skills/${safe}/meta`)).filter((e) => e.type === 'file' && e.name.endsWith('.json'));
205
+ for (const metaEntry of metaFiles) {
206
+ const remoteMeta = `library/skills/${safe}/meta/${metaEntry.name}`;
207
+ const localMeta = join(root, 'skills', safe, 'meta', metaEntry.name);
208
+ const metaRaw = await remote.readJson(remoteMeta);
209
+ if (!metaRaw || typeof metaRaw !== 'object')
210
+ continue;
211
+ await writeAtomic(localMeta, JSON.stringify(metaRaw, null, 2) + '\n');
212
+ const versions = metaRaw
213
+ .versions ?? [];
214
+ for (const v of versions) {
215
+ for (const f of v.files ?? []) {
216
+ const rel = `skills/${safe}/versions/${v.hash}/${f.rel}`;
217
+ const target = join(root, ...rel.split('/'));
218
+ if (await localHash(target)) {
219
+ skipped++;
220
+ continue;
221
+ }
222
+ const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
223
+ try {
224
+ await mkdir(dirname(target), { recursive: true });
225
+ await remote.readFile(`library/${rel}`, tmp);
226
+ await rename(tmp, target);
227
+ }
228
+ catch (err) {
229
+ await unlink(tmp).catch(() => { });
230
+ if (isNotFound(err)) {
231
+ console.warn(`[trove] 远端缺少技能文件,已跳过: ${rel}`);
232
+ continue;
233
+ }
234
+ throw err;
235
+ }
236
+ downloaded++;
237
+ bytes += (await stat(target)).size;
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+ // lessons:共享沉淀,没有设备分片。本地已有就不覆盖(本机内容可能是刚 learn 的新版本),
244
+ // 只在缺失时从远端恢复 —— 新设备能拿到经验库,又不会丢掉本机刚生成的内容。
245
+ const remoteLessons = await walkRemote(remote, 'library/lessons');
246
+ for (const remotePath of remoteLessons) {
247
+ throwIfAborted(opts.signal);
248
+ const rel = remotePath.slice('library/'.length);
249
+ const target = join(root, ...rel.split('/'));
250
+ if (await localHash(target)) {
251
+ skipped++;
252
+ continue;
253
+ }
254
+ const tmp = `${target}.tmp-${process.pid}-${Date.now()}`;
255
+ try {
256
+ await mkdir(dirname(target), { recursive: true });
257
+ await remote.readFile(remotePath, tmp);
258
+ await rename(tmp, target);
259
+ }
260
+ catch (err) {
261
+ await unlink(tmp).catch(() => { });
262
+ throw err;
263
+ }
264
+ downloaded++;
265
+ bytes += (await stat(target)).size;
266
+ progress.item(rel, itemNo, remoteLessons.length, bytes);
267
+ }
268
+ await rebuildUnifiedIndex(vaultRoot);
269
+ return { downloaded, bytes, skipped };
270
+ }
271
+ //# sourceMappingURL=sync.js.map