@vortex-os/base 0.1.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +113 -77
- package/dist/index.d.ts +1609 -166
- package/dist/index.js +3369 -528
- package/dist/index.js.map +1 -1
- package/package.json +63 -61
package/dist/index.js
CHANGED
|
@@ -9,12 +9,15 @@ var dist_exports = {};
|
|
|
9
9
|
__export(dist_exports, {
|
|
10
10
|
Privacy: () => Privacy,
|
|
11
11
|
isVisibleAt: () => isVisibleAt,
|
|
12
|
+
loadVortexConfig: () => loadVortexConfig,
|
|
12
13
|
makeContext: () => makeContext,
|
|
13
14
|
maxPrivacy: () => maxPrivacy,
|
|
14
15
|
moduleDir: () => moduleDir,
|
|
15
16
|
normalizePrivacy: () => normalizePrivacy,
|
|
16
17
|
parseFrontmatter: () => parseFrontmatter,
|
|
17
|
-
|
|
18
|
+
resolveEnvironment: () => resolveEnvironment,
|
|
19
|
+
serializeFrontmatter: () => serializeFrontmatter,
|
|
20
|
+
vortexConfigPath: () => vortexConfigPath
|
|
18
21
|
});
|
|
19
22
|
|
|
20
23
|
// ../core/dist/types.js
|
|
@@ -93,6 +96,54 @@ function moduleDir(ctx, moduleName) {
|
|
|
93
96
|
return join(ctx.modulesDir, moduleName);
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
// ../core/dist/config.js
|
|
100
|
+
import { existsSync, readFileSync } from "fs";
|
|
101
|
+
import { join as join2 } from "path";
|
|
102
|
+
var DEFAULT_CONFIG = {
|
|
103
|
+
autoRecord: { sessionStart: true, worklog: true, decision: true, ambientRecall: true, archive: true },
|
|
104
|
+
environments: []
|
|
105
|
+
};
|
|
106
|
+
function vortexConfigPath(ctx) {
|
|
107
|
+
return join2(ctx.agentDir, "vortex.json");
|
|
108
|
+
}
|
|
109
|
+
function loadVortexConfig(ctx) {
|
|
110
|
+
const path = vortexConfigPath(ctx);
|
|
111
|
+
if (!existsSync(path))
|
|
112
|
+
return DEFAULT_CONFIG;
|
|
113
|
+
try {
|
|
114
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
115
|
+
return {
|
|
116
|
+
autoRecord: { ...DEFAULT_CONFIG.autoRecord, ...raw.autoRecord ?? {} },
|
|
117
|
+
environments: Array.isArray(raw.environments) ? raw.environments : []
|
|
118
|
+
};
|
|
119
|
+
} catch {
|
|
120
|
+
return DEFAULT_CONFIG;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function resolveEnvironment(config, signals) {
|
|
124
|
+
const host = signals.hostname?.toLowerCase();
|
|
125
|
+
const env = signals.env ?? {};
|
|
126
|
+
const pathExists = signals.pathExists ?? (() => false);
|
|
127
|
+
for (const rule of config.environments) {
|
|
128
|
+
if (rule.pathExists && pathExists(rule.pathExists))
|
|
129
|
+
return rule.label;
|
|
130
|
+
if (rule.hostname && host && rule.hostname.toLowerCase() === host)
|
|
131
|
+
return rule.label;
|
|
132
|
+
if (rule.envVar) {
|
|
133
|
+
if (typeof rule.envVar === "string") {
|
|
134
|
+
if (env[rule.envVar] !== void 0)
|
|
135
|
+
return rule.label;
|
|
136
|
+
} else {
|
|
137
|
+
const v2 = env[rule.envVar.name];
|
|
138
|
+
if (v2 !== void 0 && (rule.envVar.equals === void 0 || v2 === rule.envVar.equals)) {
|
|
139
|
+
return rule.label;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
96
147
|
// ../modules/slash-commands/dist/index.js
|
|
97
148
|
var dist_exports2 = {};
|
|
98
149
|
__export(dist_exports2, {
|
|
@@ -188,7 +239,7 @@ var MemoryType = {
|
|
|
188
239
|
|
|
189
240
|
// ../modules/memory-system/dist/store.js
|
|
190
241
|
import { readdir, readFile, writeFile, mkdir, unlink, stat } from "fs/promises";
|
|
191
|
-
import { join as
|
|
242
|
+
import { join as join3, basename, extname } from "path";
|
|
192
243
|
var MemoryStore = class {
|
|
193
244
|
dir;
|
|
194
245
|
constructor(dir) {
|
|
@@ -246,13 +297,13 @@ var MemoryStore = class {
|
|
|
246
297
|
}
|
|
247
298
|
/** Absolute path of a memory file (file may not exist). */
|
|
248
299
|
pathFor(id) {
|
|
249
|
-
return
|
|
300
|
+
return join3(this.dir, `${id}.md`);
|
|
250
301
|
}
|
|
251
302
|
};
|
|
252
303
|
|
|
253
304
|
// ../modules/memory-system/dist/memory-index.js
|
|
254
305
|
import { writeFile as writeFile2 } from "fs/promises";
|
|
255
|
-
import { join as
|
|
306
|
+
import { join as join4 } from "path";
|
|
256
307
|
async function writeMemoryIndex(store, options = {}) {
|
|
257
308
|
const ids = await store.list();
|
|
258
309
|
const lines = [];
|
|
@@ -264,7 +315,7 @@ async function writeMemoryIndex(store, options = {}) {
|
|
|
264
315
|
const memory = await store.read(id);
|
|
265
316
|
lines.push(`- [${memory.frontmatter.name}](${id}.md) \u2014 ${memory.frontmatter.description}`);
|
|
266
317
|
}
|
|
267
|
-
await writeFile2(
|
|
318
|
+
await writeFile2(join4(store.dir, "MEMORY.md"), `${lines.join("\n")}
|
|
268
319
|
`, "utf8");
|
|
269
320
|
}
|
|
270
321
|
|
|
@@ -301,7 +352,7 @@ __export(dist_exports4, {
|
|
|
301
352
|
|
|
302
353
|
// ../modules/data-lint/dist/runner.js
|
|
303
354
|
import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
|
|
304
|
-
import { join as
|
|
355
|
+
import { join as join5 } from "path";
|
|
305
356
|
async function lintDirectory(options) {
|
|
306
357
|
const start = Date.now();
|
|
307
358
|
const extensions = options.extensions ?? [".md"];
|
|
@@ -338,7 +389,7 @@ async function collectFiles(dir, extensions) {
|
|
|
338
389
|
throw e;
|
|
339
390
|
}
|
|
340
391
|
for (const entry of entries) {
|
|
341
|
-
const full =
|
|
392
|
+
const full = join5(current, entry.name);
|
|
342
393
|
if (entry.isDirectory()) {
|
|
343
394
|
stack.push(full);
|
|
344
395
|
} else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
@@ -399,7 +450,7 @@ function privacyValid() {
|
|
|
399
450
|
|
|
400
451
|
// ../modules/data-lint/dist/rules/wiki-link-resolves.js
|
|
401
452
|
import { readdir as readdir3 } from "fs/promises";
|
|
402
|
-
import { basename as basename2, extname as extname2, join as
|
|
453
|
+
import { basename as basename2, extname as extname2, join as join6 } from "path";
|
|
403
454
|
var WIKI_LINK = /\[\[([^\]|#]+)(?:[|#][^\]]*)?\]\]/g;
|
|
404
455
|
function wikiLinkResolves(options) {
|
|
405
456
|
let cache;
|
|
@@ -420,7 +471,7 @@ function wikiLinkResolves(options) {
|
|
|
420
471
|
continue;
|
|
421
472
|
}
|
|
422
473
|
for (const entry of entries) {
|
|
423
|
-
const full =
|
|
474
|
+
const full = join6(current, entry.name);
|
|
424
475
|
if (entry.isDirectory()) {
|
|
425
476
|
stack.push(full);
|
|
426
477
|
} else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
@@ -465,7 +516,7 @@ __export(dist_exports5, {
|
|
|
465
516
|
|
|
466
517
|
// ../modules/ai-coding-pitfalls/dist/catalog.js
|
|
467
518
|
import { readdir as readdir4, readFile as readFile4 } from "fs/promises";
|
|
468
|
-
import { basename as basename3, extname as extname3, join as
|
|
519
|
+
import { basename as basename3, extname as extname3, join as join7 } from "path";
|
|
469
520
|
var PitfallCatalog = class {
|
|
470
521
|
dir;
|
|
471
522
|
constructor(dir) {
|
|
@@ -493,7 +544,7 @@ var PitfallCatalog = class {
|
|
|
493
544
|
async entries() {
|
|
494
545
|
try {
|
|
495
546
|
const names = await readdir4(this.dir);
|
|
496
|
-
return names.filter((n) => n.endsWith(".md")).map((n) =>
|
|
547
|
+
return names.filter((n) => n.endsWith(".md")).map((n) => join7(this.dir, n));
|
|
497
548
|
} catch (e) {
|
|
498
549
|
if (e.code === "ENOENT")
|
|
499
550
|
return [];
|
|
@@ -525,7 +576,7 @@ __export(dist_exports6, {
|
|
|
525
576
|
|
|
526
577
|
// ../modules/tool-rules/dist/catalog.js
|
|
527
578
|
import { readdir as readdir5, readFile as readFile5 } from "fs/promises";
|
|
528
|
-
import { basename as basename4, extname as extname4, join as
|
|
579
|
+
import { basename as basename4, extname as extname4, join as join8 } from "path";
|
|
529
580
|
var ToolRuleCatalog = class {
|
|
530
581
|
dir;
|
|
531
582
|
constructor(dir) {
|
|
@@ -558,7 +609,7 @@ var ToolRuleCatalog = class {
|
|
|
558
609
|
async entries() {
|
|
559
610
|
try {
|
|
560
611
|
const names = await readdir5(this.dir);
|
|
561
|
-
return names.filter((n) => n.endsWith(".md")).map((n) =>
|
|
612
|
+
return names.filter((n) => n.endsWith(".md")).map((n) => join8(this.dir, n));
|
|
562
613
|
} catch (e) {
|
|
563
614
|
if (e.code === "ENOENT")
|
|
564
615
|
return [];
|
|
@@ -1813,20 +1864,20 @@ async function renderHtml(source, options) {
|
|
|
1813
1864
|
};
|
|
1814
1865
|
}
|
|
1815
1866
|
|
|
1816
|
-
// ../modules/
|
|
1867
|
+
// ../modules/worklog/dist/index.js
|
|
1817
1868
|
var dist_exports8 = {};
|
|
1818
1869
|
__export(dist_exports8, {
|
|
1819
|
-
|
|
1870
|
+
WorklogStore: () => WorklogStore,
|
|
1820
1871
|
appendSection: () => appendSection
|
|
1821
1872
|
});
|
|
1822
1873
|
|
|
1823
|
-
// ../modules/
|
|
1874
|
+
// ../modules/worklog/dist/store.js
|
|
1824
1875
|
import { readdir as readdir6, readFile as readFile6, stat as stat2 } from "fs/promises";
|
|
1825
|
-
import { join as
|
|
1876
|
+
import { join as join9 } from "path";
|
|
1826
1877
|
var FILENAME_PATTERN = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
|
|
1827
1878
|
var MONTH_PATTERN = /^\d{2}$/;
|
|
1828
1879
|
var YEAR_PATTERN = /^\d{4}$/;
|
|
1829
|
-
var
|
|
1880
|
+
var WorklogStore = class {
|
|
1830
1881
|
rootDir;
|
|
1831
1882
|
constructor(rootDir) {
|
|
1832
1883
|
this.rootDir = rootDir;
|
|
@@ -1836,17 +1887,17 @@ var TilStore = class {
|
|
|
1836
1887
|
const years = await this.listSubdirs(this.rootDir, YEAR_PATTERN);
|
|
1837
1888
|
const entries = [];
|
|
1838
1889
|
for (const year of years) {
|
|
1839
|
-
const yearDir =
|
|
1890
|
+
const yearDir = join9(this.rootDir, year);
|
|
1840
1891
|
const months = await this.listSubdirs(yearDir, MONTH_PATTERN);
|
|
1841
1892
|
for (const month of months) {
|
|
1842
|
-
entries.push(...await this.entriesIn(
|
|
1893
|
+
entries.push(...await this.entriesIn(join9(yearDir, month)));
|
|
1843
1894
|
}
|
|
1844
1895
|
}
|
|
1845
1896
|
return entries.sort((a, b2) => a.date === b2.date ? a.keyword.localeCompare(b2.keyword) : a.date.localeCompare(b2.date));
|
|
1846
1897
|
}
|
|
1847
1898
|
/** Entries within one calendar month. */
|
|
1848
1899
|
async listByMonth(year, month) {
|
|
1849
|
-
const monthDir =
|
|
1900
|
+
const monthDir = join9(this.rootDir, String(year).padStart(4, "0"), String(month).padStart(2, "0"));
|
|
1850
1901
|
const entries = await this.entriesIn(monthDir);
|
|
1851
1902
|
return entries.sort((a, b2) => a.date === b2.date ? a.keyword.localeCompare(b2.keyword) : a.date.localeCompare(b2.date));
|
|
1852
1903
|
}
|
|
@@ -1859,7 +1910,7 @@ var TilStore = class {
|
|
|
1859
1910
|
const [year, month] = date.split("-");
|
|
1860
1911
|
if (!year || !month)
|
|
1861
1912
|
return void 0;
|
|
1862
|
-
const monthDir =
|
|
1913
|
+
const monthDir = join9(this.rootDir, year, month);
|
|
1863
1914
|
const entries = (await this.entriesIn(monthDir)).filter((e) => e.date === date).sort((a, b2) => a.keyword.localeCompare(b2.keyword));
|
|
1864
1915
|
return entries[0];
|
|
1865
1916
|
}
|
|
@@ -1876,7 +1927,7 @@ var TilStore = class {
|
|
|
1876
1927
|
if (!year || !month) {
|
|
1877
1928
|
throw new Error(`Invalid date: ${date} (expected YYYY-MM-DD)`);
|
|
1878
1929
|
}
|
|
1879
|
-
return
|
|
1930
|
+
return join9(this.rootDir, year, month, `${date}-${keyword}.md`);
|
|
1880
1931
|
}
|
|
1881
1932
|
async listSubdirs(dir, pattern) {
|
|
1882
1933
|
try {
|
|
@@ -1902,7 +1953,7 @@ var TilStore = class {
|
|
|
1902
1953
|
const match = name.match(FILENAME_PATTERN);
|
|
1903
1954
|
if (!match)
|
|
1904
1955
|
continue;
|
|
1905
|
-
const path =
|
|
1956
|
+
const path = join9(monthDir, name);
|
|
1906
1957
|
try {
|
|
1907
1958
|
const info = await stat2(path);
|
|
1908
1959
|
if (!info.isFile())
|
|
@@ -1920,7 +1971,7 @@ var TilStore = class {
|
|
|
1920
1971
|
}
|
|
1921
1972
|
};
|
|
1922
1973
|
|
|
1923
|
-
// ../modules/
|
|
1974
|
+
// ../modules/worklog/dist/append.js
|
|
1924
1975
|
import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
1925
1976
|
async function appendSection(entry, title, body) {
|
|
1926
1977
|
const original = await readFile7(entry.path, "utf8");
|
|
@@ -1945,7 +1996,7 @@ __export(dist_exports9, {
|
|
|
1945
1996
|
|
|
1946
1997
|
// ../modules/decision-log/dist/store.js
|
|
1947
1998
|
import { readdir as readdir7, readFile as readFile8, stat as stat3 } from "fs/promises";
|
|
1948
|
-
import { join as
|
|
1999
|
+
import { join as join10 } from "path";
|
|
1949
2000
|
var FILENAME_PATTERN2 = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
|
|
1950
2001
|
var DecisionStore = class {
|
|
1951
2002
|
rootDir;
|
|
@@ -1967,7 +2018,7 @@ var DecisionStore = class {
|
|
|
1967
2018
|
const match = name.match(FILENAME_PATTERN2);
|
|
1968
2019
|
if (!match)
|
|
1969
2020
|
continue;
|
|
1970
|
-
const path =
|
|
2021
|
+
const path = join10(this.rootDir, name);
|
|
1971
2022
|
try {
|
|
1972
2023
|
const info = await stat3(path);
|
|
1973
2024
|
if (!info.isFile())
|
|
@@ -2038,7 +2089,7 @@ var DecisionStore = class {
|
|
|
2038
2089
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
2039
2090
|
throw new Error(`Invalid date: ${date} (expected YYYY-MM-DD)`);
|
|
2040
2091
|
}
|
|
2041
|
-
return
|
|
2092
|
+
return join10(this.rootDir, `${date}-${slug}.md`);
|
|
2042
2093
|
}
|
|
2043
2094
|
};
|
|
2044
2095
|
|
|
@@ -2102,7 +2153,7 @@ __export(dist_exports10, {
|
|
|
2102
2153
|
|
|
2103
2154
|
// ../modules/index-generator/dist/scan.js
|
|
2104
2155
|
import { readdir as readdir8, readFile as readFile9, stat as stat4 } from "fs/promises";
|
|
2105
|
-
import { basename as basename5, extname as extname5, join as
|
|
2156
|
+
import { basename as basename5, extname as extname5, join as join11, relative } from "path";
|
|
2106
2157
|
var RESERVED_FILES = /* @__PURE__ */ new Set(["README.md", "_INDEX.md"]);
|
|
2107
2158
|
var H1_PATTERN = /^#\s+(.+?)\s*$/m;
|
|
2108
2159
|
async function scanDirectory(rootDir, opts = {}) {
|
|
@@ -2132,7 +2183,7 @@ async function walk(rootDir, currentDir, recursive, skipFilenames, skipPrefixes,
|
|
|
2132
2183
|
continue;
|
|
2133
2184
|
if (name.startsWith(".") || name.startsWith("_"))
|
|
2134
2185
|
continue;
|
|
2135
|
-
await walk(rootDir,
|
|
2186
|
+
await walk(rootDir, join11(currentDir, name), recursive, skipFilenames, skipPrefixes, out);
|
|
2136
2187
|
continue;
|
|
2137
2188
|
}
|
|
2138
2189
|
if (!dirent.isFile())
|
|
@@ -2144,7 +2195,7 @@ async function walk(rootDir, currentDir, recursive, skipFilenames, skipPrefixes,
|
|
|
2144
2195
|
const nameNoExt = basename5(name, ".md");
|
|
2145
2196
|
if (skipPrefixes.some((p) => nameNoExt.startsWith(p)))
|
|
2146
2197
|
continue;
|
|
2147
|
-
const fullPath =
|
|
2198
|
+
const fullPath = join11(currentDir, name);
|
|
2148
2199
|
let info;
|
|
2149
2200
|
try {
|
|
2150
2201
|
info = await stat4(fullPath);
|
|
@@ -2254,7 +2305,7 @@ function today() {
|
|
|
2254
2305
|
|
|
2255
2306
|
// ../modules/index-generator/dist/nested.js
|
|
2256
2307
|
import { readdir as readdir9, stat as stat5 } from "fs/promises";
|
|
2257
|
-
import { extname as extname6, join as
|
|
2308
|
+
import { extname as extname6, join as join12 } from "path";
|
|
2258
2309
|
async function findIndexableDirs(rootDir, options = {}) {
|
|
2259
2310
|
const minEntries = options.minEntries ?? 1;
|
|
2260
2311
|
const skipPrefixes = options.skipPrefixes ?? [];
|
|
@@ -2290,7 +2341,7 @@ async function walk2(dir, minEntries, skipPrefixes, out) {
|
|
|
2290
2341
|
if (skipPrefixes.some((p) => nameNoExt.startsWith(p)))
|
|
2291
2342
|
continue;
|
|
2292
2343
|
try {
|
|
2293
|
-
const info = await stat5(
|
|
2344
|
+
const info = await stat5(join12(dir, dirent.name));
|
|
2294
2345
|
if (!info.isFile())
|
|
2295
2346
|
continue;
|
|
2296
2347
|
} catch {
|
|
@@ -2302,7 +2353,7 @@ async function walk2(dir, minEntries, skipPrefixes, out) {
|
|
|
2302
2353
|
out.push(dir);
|
|
2303
2354
|
}
|
|
2304
2355
|
for (const name of subdirs) {
|
|
2305
|
-
await walk2(
|
|
2356
|
+
await walk2(join12(dir, name), minEntries, skipPrefixes, out);
|
|
2306
2357
|
}
|
|
2307
2358
|
}
|
|
2308
2359
|
|
|
@@ -2314,7 +2365,7 @@ __export(dist_exports11, {
|
|
|
2314
2365
|
|
|
2315
2366
|
// ../modules/runbooks/dist/store.js
|
|
2316
2367
|
import { readdir as readdir10, readFile as readFile10, stat as stat6 } from "fs/promises";
|
|
2317
|
-
import { extname as extname7, join as
|
|
2368
|
+
import { extname as extname7, join as join13 } from "path";
|
|
2318
2369
|
var RESERVED_FILES2 = /* @__PURE__ */ new Set(["README.md", "_INDEX.md"]);
|
|
2319
2370
|
var RunbookStore = class {
|
|
2320
2371
|
rootDir;
|
|
@@ -2337,7 +2388,7 @@ var RunbookStore = class {
|
|
|
2337
2388
|
continue;
|
|
2338
2389
|
if (RESERVED_FILES2.has(name))
|
|
2339
2390
|
continue;
|
|
2340
|
-
const path =
|
|
2391
|
+
const path = join13(this.rootDir, name);
|
|
2341
2392
|
try {
|
|
2342
2393
|
const info = await stat6(path);
|
|
2343
2394
|
if (!info.isFile())
|
|
@@ -2446,7 +2497,7 @@ function extractWikiLinks(body) {
|
|
|
2446
2497
|
|
|
2447
2498
|
// ../modules/link-rewriter/dist/resolve.js
|
|
2448
2499
|
import { readdir as readdir11, stat as stat7 } from "fs/promises";
|
|
2449
|
-
import { basename as basename6, dirname, extname as extname8, isAbsolute, join as
|
|
2500
|
+
import { basename as basename6, dirname, extname as extname8, isAbsolute, join as join14, relative as relative2, resolve as pathResolve } from "path";
|
|
2450
2501
|
async function buildFileIndex(rootDir, options = {}) {
|
|
2451
2502
|
const byBasename = /* @__PURE__ */ new Map();
|
|
2452
2503
|
const byRelPath = /* @__PURE__ */ new Map();
|
|
@@ -2477,7 +2528,7 @@ async function walk3(rootDir, dir, byBasename, byRelPath, additionalExts) {
|
|
|
2477
2528
|
if (dirent.isDirectory()) {
|
|
2478
2529
|
if (name.startsWith("."))
|
|
2479
2530
|
continue;
|
|
2480
|
-
await walk3(rootDir,
|
|
2531
|
+
await walk3(rootDir, join14(dir, name), byBasename, byRelPath, additionalExts);
|
|
2481
2532
|
continue;
|
|
2482
2533
|
}
|
|
2483
2534
|
if (!dirent.isFile())
|
|
@@ -2486,7 +2537,7 @@ async function walk3(rootDir, dir, byBasename, byRelPath, additionalExts) {
|
|
|
2486
2537
|
const isMd = ext === ".md";
|
|
2487
2538
|
if (!isMd && !additionalExts.has(ext))
|
|
2488
2539
|
continue;
|
|
2489
|
-
const path =
|
|
2540
|
+
const path = join14(dir, name);
|
|
2490
2541
|
try {
|
|
2491
2542
|
const info = await stat7(path);
|
|
2492
2543
|
if (!info.isFile())
|
|
@@ -2657,254 +2708,2173 @@ function rewriteBody(body, redirections) {
|
|
|
2657
2708
|
return { newBody: out, fileRewrites };
|
|
2658
2709
|
}
|
|
2659
2710
|
|
|
2660
|
-
// ../
|
|
2711
|
+
// ../modules/proactive-curator/dist/index.js
|
|
2661
2712
|
var dist_exports13 = {};
|
|
2662
2713
|
__export(dist_exports13, {
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2714
|
+
AmbientBackpressure: () => AmbientBackpressure,
|
|
2715
|
+
AmbientRecaller: () => AmbientRecaller,
|
|
2716
|
+
ClaudeCodeLLMJudge: () => ClaudeCodeLLMJudge,
|
|
2717
|
+
ClaudeCodeLLMJudgeError: () => ClaudeCodeLLMJudgeError,
|
|
2718
|
+
ClaudeDesktopLLMJudge: () => ClaudeDesktopLLMJudge,
|
|
2719
|
+
ClaudeDesktopLLMJudgeError: () => ClaudeDesktopLLMJudgeError,
|
|
2720
|
+
CodexLLMJudge: () => CodexLLMJudge,
|
|
2721
|
+
CodexLLMJudgeError: () => CodexLLMJudgeError,
|
|
2722
|
+
GeminiLLMJudge: () => GeminiLLMJudge,
|
|
2723
|
+
GeminiLLMJudgeError: () => GeminiLLMJudgeError,
|
|
2724
|
+
HubProposer: () => HubProposer,
|
|
2725
|
+
InjectedLLMJudge: () => InjectedLLMJudge,
|
|
2726
|
+
InsightProposer: () => InsightProposer,
|
|
2727
|
+
LLMJudgeError: () => LLMJudgeError,
|
|
2728
|
+
TurnBuffer: () => TurnBuffer,
|
|
2729
|
+
computeFingerprint: () => computeFingerprint,
|
|
2730
|
+
deriveQueryFromTurns: () => deriveQueryFromTurns,
|
|
2731
|
+
frameForJudge: () => frameForJudge,
|
|
2732
|
+
loadDeclinedFingerprints: () => loadDeclinedFingerprints,
|
|
2733
|
+
parseJudgeResponse: () => parseJudgeResponse,
|
|
2734
|
+
recordAcceptance: () => recordAcceptance,
|
|
2735
|
+
recordDecline: () => recordDecline,
|
|
2736
|
+
resetDeclined: () => resetDeclined
|
|
2669
2737
|
});
|
|
2670
2738
|
|
|
2671
|
-
// ../
|
|
2672
|
-
import {
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2739
|
+
// ../modules/proactive-curator/dist/fingerprint.js
|
|
2740
|
+
import { createHash } from "crypto";
|
|
2741
|
+
function computeFingerprint(input) {
|
|
2742
|
+
const normalized = normalizeForHash(input);
|
|
2743
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
2744
|
+
}
|
|
2745
|
+
function normalizeForHash(input) {
|
|
2746
|
+
if (input.kind === "capture-insight") {
|
|
2747
|
+
return JSON.stringify({
|
|
2748
|
+
kind: "capture-insight",
|
|
2749
|
+
turnIds: [...input.turnIds].sort(),
|
|
2750
|
+
topic: normalizeTopic(input.topic),
|
|
2751
|
+
actionKind: input.actionKind,
|
|
2752
|
+
targetPath: normalizePath(input.targetPath)
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
return JSON.stringify({
|
|
2756
|
+
kind: "create-hub",
|
|
2757
|
+
topic: normalizeTopic(input.topic),
|
|
2758
|
+
sourceDocs: [...input.sourceDocs].map(normalizePath).sort(),
|
|
2759
|
+
actionKind: input.actionKind,
|
|
2760
|
+
targetPath: normalizePath(input.targetPath)
|
|
2761
|
+
});
|
|
2762
|
+
}
|
|
2763
|
+
function normalizeTopic(t) {
|
|
2764
|
+
return t.trim().toLowerCase();
|
|
2765
|
+
}
|
|
2766
|
+
function normalizePath(p) {
|
|
2767
|
+
return p.replace(/\\/g, "/");
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// ../modules/proactive-curator/dist/decline-store.js
|
|
2771
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2772
|
+
import { appendFile, mkdir as mkdir2, readFile as readFile13, writeFile as writeFile5 } from "fs/promises";
|
|
2773
|
+
import { join as join15 } from "path";
|
|
2774
|
+
var STORE_DIR = "data/_proactive-curator";
|
|
2775
|
+
var DECLINED_FILE = "declined.json";
|
|
2776
|
+
var ACCEPTED_LOG = "accepted.log";
|
|
2777
|
+
var DEFAULT_EXPIRY_DAYS = 30;
|
|
2778
|
+
async function loadDeclinedFingerprints(cwd, now) {
|
|
2779
|
+
const parsed = await readDeclinedFile(cwd);
|
|
2780
|
+
if (!parsed)
|
|
2781
|
+
return /* @__PURE__ */ new Set();
|
|
2782
|
+
const active = /* @__PURE__ */ new Set();
|
|
2783
|
+
const nowMs = now.getTime();
|
|
2784
|
+
for (const kind of ["capture-insight", "create-hub"]) {
|
|
2785
|
+
const entries = parsed[kind] ?? {};
|
|
2786
|
+
for (const [fp, entry] of Object.entries(entries)) {
|
|
2787
|
+
if (isActive(entry, nowMs))
|
|
2788
|
+
active.add(fp);
|
|
2686
2789
|
}
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2790
|
+
}
|
|
2791
|
+
return active;
|
|
2792
|
+
}
|
|
2793
|
+
async function recordDecline(cwd, args) {
|
|
2794
|
+
await ensureStoreDir(cwd);
|
|
2795
|
+
const existing = await readDeclinedFile(cwd) ?? emptyDeclinedFile();
|
|
2796
|
+
const expiryDays = args.expiryDays ?? DEFAULT_EXPIRY_DAYS;
|
|
2797
|
+
const expiresAt = new Date(args.now.getTime() + expiryDays * 24 * 60 * 60 * 1e3).toISOString();
|
|
2798
|
+
const updated = {
|
|
2799
|
+
"capture-insight": purgeExpired(existing["capture-insight"], args.now),
|
|
2800
|
+
"create-hub": purgeExpired(existing["create-hub"], args.now)
|
|
2801
|
+
};
|
|
2802
|
+
const entry = {
|
|
2803
|
+
declinedAt: args.now.toISOString(),
|
|
2804
|
+
topic: args.topic,
|
|
2805
|
+
expiresAt,
|
|
2806
|
+
actionKind: args.actionKind,
|
|
2807
|
+
targetPath: args.targetPath,
|
|
2808
|
+
...args.sourceDocs ? { sourceDocs: args.sourceDocs } : {}
|
|
2809
|
+
};
|
|
2810
|
+
updated[args.kind] = { ...updated[args.kind], [args.fingerprint]: entry };
|
|
2811
|
+
await writeFile5(join15(cwd, STORE_DIR, DECLINED_FILE), JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
2812
|
+
}
|
|
2813
|
+
async function recordAcceptance(cwd, args) {
|
|
2814
|
+
await ensureStoreDir(cwd);
|
|
2815
|
+
const line = JSON.stringify({
|
|
2816
|
+
acceptedAt: args.now.toISOString(),
|
|
2817
|
+
kind: args.kind,
|
|
2818
|
+
fingerprint: args.fingerprint,
|
|
2819
|
+
topic: args.topic,
|
|
2820
|
+
actionKind: args.actionKind,
|
|
2821
|
+
writtenPath: args.writtenPath
|
|
2822
|
+
});
|
|
2823
|
+
await appendFile(join15(cwd, STORE_DIR, ACCEPTED_LOG), line + "\n", "utf8");
|
|
2824
|
+
}
|
|
2825
|
+
async function resetDeclined(cwd, kind) {
|
|
2826
|
+
const file = join15(cwd, STORE_DIR, DECLINED_FILE);
|
|
2827
|
+
if (!existsSync2(file))
|
|
2828
|
+
return;
|
|
2829
|
+
if (kind === void 0) {
|
|
2830
|
+
await writeFile5(file, JSON.stringify(emptyDeclinedFile(), null, 2) + "\n", "utf8");
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
const parsed = await readDeclinedFile(cwd) ?? emptyDeclinedFile();
|
|
2834
|
+
parsed[kind] = {};
|
|
2835
|
+
await writeFile5(file, JSON.stringify(parsed, null, 2) + "\n", "utf8");
|
|
2836
|
+
}
|
|
2837
|
+
function isActive(entry, nowMs) {
|
|
2838
|
+
const expiresMs = new Date(entry.expiresAt).getTime();
|
|
2839
|
+
return Number.isFinite(expiresMs) && expiresMs > nowMs;
|
|
2840
|
+
}
|
|
2841
|
+
function purgeExpired(entries, now) {
|
|
2842
|
+
if (!entries)
|
|
2843
|
+
return {};
|
|
2844
|
+
const out = {};
|
|
2845
|
+
const nowMs = now.getTime();
|
|
2846
|
+
for (const [fp, entry] of Object.entries(entries)) {
|
|
2847
|
+
if (isActive(entry, nowMs))
|
|
2848
|
+
out[fp] = entry;
|
|
2849
|
+
}
|
|
2850
|
+
return out;
|
|
2851
|
+
}
|
|
2852
|
+
async function readDeclinedFile(cwd) {
|
|
2853
|
+
const file = join15(cwd, STORE_DIR, DECLINED_FILE);
|
|
2854
|
+
if (!existsSync2(file))
|
|
2855
|
+
return null;
|
|
2856
|
+
try {
|
|
2857
|
+
const raw = await readFile13(file, "utf8");
|
|
2858
|
+
const parsed = JSON.parse(raw);
|
|
2859
|
+
return {
|
|
2860
|
+
"capture-insight": parsed["capture-insight"] ?? {},
|
|
2861
|
+
"create-hub": parsed["create-hub"] ?? {}
|
|
2862
|
+
};
|
|
2863
|
+
} catch {
|
|
2864
|
+
return null;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
function emptyDeclinedFile() {
|
|
2868
|
+
return { "capture-insight": {}, "create-hub": {} };
|
|
2869
|
+
}
|
|
2870
|
+
async function ensureStoreDir(cwd) {
|
|
2871
|
+
await mkdir2(join15(cwd, STORE_DIR), { recursive: true });
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
// ../modules/proactive-curator/dist/insight-proposer.js
|
|
2875
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2876
|
+
import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile14, readdir as readdir12, writeFile as writeFile6 } from "fs/promises";
|
|
2877
|
+
import { dirname as dirname2, join as join16 } from "path";
|
|
2878
|
+
var SYSTEM_META_DIRS = /* @__PURE__ */ new Set([
|
|
2879
|
+
"worklog",
|
|
2880
|
+
"decision-log",
|
|
2881
|
+
"runbooks",
|
|
2882
|
+
"hubs",
|
|
2883
|
+
"_memory",
|
|
2884
|
+
"_templates",
|
|
2885
|
+
"_proactive-curator"
|
|
2886
|
+
]);
|
|
2887
|
+
var InsightProposer = class {
|
|
2888
|
+
options;
|
|
2889
|
+
kind = "capture-insight";
|
|
2890
|
+
constructor(options) {
|
|
2891
|
+
this.options = options;
|
|
2892
|
+
}
|
|
2893
|
+
async evaluate(input, ctx) {
|
|
2894
|
+
if (input.accumulatedTokens < this.options.minTokensAccumulated)
|
|
2895
|
+
return null;
|
|
2896
|
+
if (input.recentTurns.length < this.options.minTurnsBeforeFirstCheck)
|
|
2897
|
+
return null;
|
|
2898
|
+
const capture = await askCaptureDecision(ctx.llm, input.recentTurns);
|
|
2899
|
+
if (capture.verdict === "no" || !capture.summary || !capture.topic)
|
|
2900
|
+
return null;
|
|
2901
|
+
const tree = await scanTopicTree(ctx.cwd, this.options.maxTreeEntries ?? 100);
|
|
2902
|
+
const placement = await askPlacementDecision(ctx.llm, capture.summary, capture.topic, tree);
|
|
2903
|
+
if (!placement)
|
|
2904
|
+
return null;
|
|
2905
|
+
const action = buildAction(placement, capture.summary, capture.topic, ctx.now);
|
|
2906
|
+
if (!action)
|
|
2907
|
+
return null;
|
|
2908
|
+
const targetPath = targetPathFromAction(action);
|
|
2909
|
+
const fingerprint = computeFingerprint({
|
|
2910
|
+
kind: "capture-insight",
|
|
2911
|
+
turnIds: input.recentTurns.map((t) => t.turnId),
|
|
2912
|
+
topic: capture.topic,
|
|
2913
|
+
actionKind: action.kind,
|
|
2914
|
+
targetPath
|
|
2915
|
+
});
|
|
2916
|
+
if (ctx.declinedFingerprints.has(fingerprint))
|
|
2917
|
+
return null;
|
|
2918
|
+
const sourceRefs = input.recentTurns.map((t) => t.turnId);
|
|
2919
|
+
const declineExpiryDays = this.options.declineExpiryDays;
|
|
2920
|
+
return {
|
|
2921
|
+
kind: "capture-insight",
|
|
2922
|
+
fingerprint,
|
|
2923
|
+
action,
|
|
2924
|
+
preview: capture.summary,
|
|
2925
|
+
rationale: placement.rationale,
|
|
2926
|
+
sourceRefs,
|
|
2927
|
+
onAccept: async () => {
|
|
2928
|
+
const writtenPath = await applyAction(ctx.cwd, action);
|
|
2929
|
+
await recordAcceptance(ctx.cwd, {
|
|
2930
|
+
kind: "capture-insight",
|
|
2931
|
+
fingerprint,
|
|
2932
|
+
topic: capture.topic,
|
|
2933
|
+
actionKind: action.kind,
|
|
2934
|
+
writtenPath,
|
|
2935
|
+
now: ctx.now
|
|
2936
|
+
});
|
|
2937
|
+
return {
|
|
2938
|
+
writtenPath,
|
|
2939
|
+
actionApplied: action.kind,
|
|
2940
|
+
nextActionHint: nextActionHintFor(action)
|
|
2941
|
+
};
|
|
2942
|
+
},
|
|
2943
|
+
onDecline: async () => {
|
|
2944
|
+
await recordDecline(ctx.cwd, {
|
|
2945
|
+
kind: "capture-insight",
|
|
2946
|
+
fingerprint,
|
|
2947
|
+
topic: capture.topic,
|
|
2948
|
+
actionKind: action.kind,
|
|
2949
|
+
targetPath,
|
|
2950
|
+
now: ctx.now,
|
|
2951
|
+
...declineExpiryDays !== void 0 ? { expiryDays: declineExpiryDays } : {}
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
};
|
|
2955
|
+
}
|
|
2956
|
+
};
|
|
2957
|
+
async function askCaptureDecision(llm, turns) {
|
|
2958
|
+
const prompt = renderCapturePrompt(turns);
|
|
2959
|
+
const expected = {
|
|
2960
|
+
shape: "json",
|
|
2961
|
+
schema: {
|
|
2962
|
+
verdict: "yes|no",
|
|
2963
|
+
summary: "string (required when verdict=yes)",
|
|
2964
|
+
topic: "string (required when verdict=yes, 1-3 words)"
|
|
2690
2965
|
}
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2966
|
+
};
|
|
2967
|
+
const raw = await llm.ask(prompt, expected);
|
|
2968
|
+
return normalizeCaptureDecision(raw);
|
|
2969
|
+
}
|
|
2970
|
+
function renderCapturePrompt(turns) {
|
|
2971
|
+
const transcript = turns.map((t) => `[${t.role}] ${t.content}`).join("\n\n");
|
|
2972
|
+
return [
|
|
2973
|
+
"You are reviewing a conversation snippet to decide whether anything in it deserves to be captured as a standalone note.",
|
|
2974
|
+
"",
|
|
2975
|
+
"Capture only if the recent turns produced an insight, decision, or piece of structured thought that would be lost if the session ended right now. Routine status updates, command output, or trivial back-and-forth do not qualify.",
|
|
2976
|
+
"",
|
|
2977
|
+
"Return JSON:",
|
|
2978
|
+
` { "verdict": "yes" | "no", "summary": "3-5 sentences if verdict=yes", "topic": "1-3 words if verdict=yes" }`,
|
|
2979
|
+
"",
|
|
2980
|
+
"Recent turns:",
|
|
2981
|
+
transcript
|
|
2982
|
+
].join("\n");
|
|
2983
|
+
}
|
|
2984
|
+
function normalizeCaptureDecision(raw) {
|
|
2985
|
+
if (typeof raw !== "object" || raw === null)
|
|
2986
|
+
return { verdict: "no" };
|
|
2987
|
+
const obj = raw;
|
|
2988
|
+
if (obj.verdict !== "yes")
|
|
2989
|
+
return { verdict: "no" };
|
|
2990
|
+
const summary = typeof obj.summary === "string" ? obj.summary.trim() : "";
|
|
2991
|
+
const topic = typeof obj.topic === "string" ? obj.topic.trim() : "";
|
|
2992
|
+
if (!summary || !topic)
|
|
2993
|
+
return { verdict: "no" };
|
|
2994
|
+
return { verdict: "yes", summary, topic };
|
|
2995
|
+
}
|
|
2996
|
+
async function askPlacementDecision(llm, summary, topic, tree) {
|
|
2997
|
+
const prompt = renderPlacementPrompt(summary, topic, tree);
|
|
2998
|
+
const expected = {
|
|
2999
|
+
shape: "json",
|
|
3000
|
+
schema: {
|
|
3001
|
+
actionKind: "create-folder|create-file|append-section|update-file",
|
|
3002
|
+
rationale: "string (one line)",
|
|
3003
|
+
folderPath: "string (create-folder, create-file)",
|
|
3004
|
+
filename: "string (create-folder, create-file)",
|
|
3005
|
+
filePath: "string (append-section, update-file)",
|
|
3006
|
+
sectionHeader: "string (append-section)",
|
|
3007
|
+
reason: "string (update-file)"
|
|
2697
3008
|
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
3009
|
+
};
|
|
3010
|
+
const raw = await llm.ask(prompt, expected);
|
|
3011
|
+
return normalizePlacementDecision(raw);
|
|
3012
|
+
}
|
|
3013
|
+
function renderPlacementPrompt(summary, topic, tree) {
|
|
3014
|
+
const treeText = tree.folders.length === 0 ? "(no existing topic folders)" : tree.folders.map((f) => {
|
|
3015
|
+
const fileLines = f.files.map((file) => ` - ${file.filename}${file.frontmatterTopic ? ` [topic: ${file.frontmatterTopic}]` : ""}`).join("\n");
|
|
3016
|
+
return ` ${f.path}/
|
|
3017
|
+
${fileLines}`;
|
|
3018
|
+
}).join("\n");
|
|
3019
|
+
const truncationNote = tree.truncated ? "\n\n(Tree snapshot truncated \u2014 additional folders exist but are omitted for brevity.)" : "";
|
|
3020
|
+
return [
|
|
3021
|
+
"You are deciding where to place a captured insight in the user's existing topic tree.",
|
|
3022
|
+
"",
|
|
3023
|
+
`Insight topic: ${topic}`,
|
|
3024
|
+
`Insight summary: ${summary}`,
|
|
3025
|
+
"",
|
|
3026
|
+
"Existing topic tree (data-relative paths, system-meta dirs excluded):",
|
|
3027
|
+
treeText + truncationNote,
|
|
3028
|
+
"",
|
|
3029
|
+
"Pick one placement action. Preference order \u2014 extend existing structure before adding new:",
|
|
3030
|
+
" 1. append-section \u2014 an existing file is the right home; add a `## <sectionHeader>` section to it.",
|
|
3031
|
+
" 2. create-file \u2014 a topic folder exists; add a new sibling file alongside its current contents.",
|
|
3032
|
+
" 3. create-folder \u2014 the topic is genuinely new; create a folder and seed it with the first file.",
|
|
3033
|
+
" 4. update-file \u2014 an existing file needs in-place revision (rare; prefer append).",
|
|
3034
|
+
"",
|
|
3035
|
+
"Return JSON:",
|
|
3036
|
+
` { "actionKind": "append-section", "rationale": "one line why", "filePath": "<data-relative path>", "sectionHeader": "<section title without leading ##>" }`,
|
|
3037
|
+
` { "actionKind": "create-file", "rationale": "one line why", "folderPath": "<data-relative folder>", "filename": "<slug>.md" }`,
|
|
3038
|
+
` { "actionKind": "create-folder", "rationale": "one line why", "folderPath": "<data-relative folder>", "filename": "<slug>.md" }`,
|
|
3039
|
+
` { "actionKind": "update-file", "rationale": "one line why", "filePath": "<data-relative path>", "reason": "what needs revising" }`,
|
|
3040
|
+
"",
|
|
3041
|
+
"Paths must be relative to data/. Do not propose paths inside system-meta directories (worklog/, decision-log/, runbooks/, hubs/, _memory/, _templates/, _proactive-curator/, or any other _* directory)."
|
|
3042
|
+
].join("\n");
|
|
3043
|
+
}
|
|
3044
|
+
function normalizePlacementDecision(raw) {
|
|
3045
|
+
if (typeof raw !== "object" || raw === null)
|
|
3046
|
+
return null;
|
|
3047
|
+
const obj = raw;
|
|
3048
|
+
const actionKind = String(obj.actionKind ?? "");
|
|
3049
|
+
const rationale = typeof obj.rationale === "string" ? obj.rationale.trim() : "";
|
|
3050
|
+
if (!rationale)
|
|
3051
|
+
return null;
|
|
3052
|
+
switch (actionKind) {
|
|
3053
|
+
case "create-folder":
|
|
3054
|
+
case "create-file": {
|
|
3055
|
+
const folderPath = typeof obj.folderPath === "string" ? obj.folderPath.trim() : "";
|
|
3056
|
+
const filename = typeof obj.filename === "string" ? obj.filename.trim() : "";
|
|
3057
|
+
if (!folderPath || !filename)
|
|
3058
|
+
return null;
|
|
3059
|
+
if (isSystemMetaPath(folderPath))
|
|
3060
|
+
return null;
|
|
3061
|
+
return { actionKind, rationale, folderPath, filename };
|
|
3062
|
+
}
|
|
3063
|
+
case "append-section": {
|
|
3064
|
+
const filePath = typeof obj.filePath === "string" ? obj.filePath.trim() : "";
|
|
3065
|
+
const sectionHeader = typeof obj.sectionHeader === "string" ? obj.sectionHeader.trim() : "";
|
|
3066
|
+
if (!filePath || !sectionHeader)
|
|
3067
|
+
return null;
|
|
3068
|
+
if (isSystemMetaPath(filePath))
|
|
3069
|
+
return null;
|
|
3070
|
+
return { actionKind: "append-section", rationale, filePath, sectionHeader };
|
|
3071
|
+
}
|
|
3072
|
+
case "update-file": {
|
|
3073
|
+
const filePath = typeof obj.filePath === "string" ? obj.filePath.trim() : "";
|
|
3074
|
+
const reason = typeof obj.reason === "string" ? obj.reason.trim() : "";
|
|
3075
|
+
if (!filePath || !reason)
|
|
3076
|
+
return null;
|
|
3077
|
+
if (isSystemMetaPath(filePath))
|
|
3078
|
+
return null;
|
|
3079
|
+
return { actionKind: "update-file", rationale, filePath, reason };
|
|
3080
|
+
}
|
|
3081
|
+
default:
|
|
3082
|
+
return null;
|
|
2701
3083
|
}
|
|
2702
|
-
}
|
|
2703
|
-
function
|
|
2704
|
-
const
|
|
2705
|
-
|
|
2706
|
-
|
|
3084
|
+
}
|
|
3085
|
+
function isSystemMetaPath(p) {
|
|
3086
|
+
const normalized = p.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
3087
|
+
const first = normalized.split("/")[0] ?? "";
|
|
3088
|
+
if (SYSTEM_META_DIRS.has(first))
|
|
3089
|
+
return true;
|
|
3090
|
+
if (first.startsWith("_"))
|
|
3091
|
+
return true;
|
|
3092
|
+
return false;
|
|
3093
|
+
}
|
|
3094
|
+
function buildAction(placement, summary, topic, now) {
|
|
3095
|
+
const body = renderCaptureBody(summary, topic, now);
|
|
3096
|
+
switch (placement.actionKind) {
|
|
3097
|
+
case "create-folder":
|
|
3098
|
+
case "create-file":
|
|
3099
|
+
if (!placement.folderPath || !placement.filename)
|
|
3100
|
+
return null;
|
|
3101
|
+
return {
|
|
3102
|
+
kind: placement.actionKind,
|
|
3103
|
+
folderPath: placement.folderPath,
|
|
3104
|
+
filename: placement.filename,
|
|
3105
|
+
body
|
|
3106
|
+
};
|
|
3107
|
+
case "append-section":
|
|
3108
|
+
if (!placement.filePath || !placement.sectionHeader)
|
|
3109
|
+
return null;
|
|
3110
|
+
return {
|
|
3111
|
+
kind: "append-section",
|
|
3112
|
+
filePath: placement.filePath,
|
|
3113
|
+
sectionHeader: placement.sectionHeader,
|
|
3114
|
+
body: renderSectionBody(summary, now)
|
|
3115
|
+
};
|
|
3116
|
+
case "update-file":
|
|
3117
|
+
if (!placement.filePath || !placement.reason)
|
|
3118
|
+
return null;
|
|
3119
|
+
return {
|
|
3120
|
+
kind: "update-file",
|
|
3121
|
+
filePath: placement.filePath,
|
|
3122
|
+
body: renderUpdateBody(summary, topic, placement.reason, now),
|
|
3123
|
+
reason: placement.reason
|
|
3124
|
+
};
|
|
2707
3125
|
}
|
|
2708
|
-
return trimmed;
|
|
2709
3126
|
}
|
|
2710
|
-
function
|
|
2711
|
-
const
|
|
3127
|
+
function renderCaptureBody(summary, topic, now) {
|
|
3128
|
+
const today2 = formatYmd(now);
|
|
3129
|
+
return `---
|
|
3130
|
+
type: note
|
|
3131
|
+
topic: ${topic}
|
|
3132
|
+
created: ${today2}
|
|
3133
|
+
updated: ${today2}
|
|
3134
|
+
tags: [note, proactive-curator]
|
|
3135
|
+
---
|
|
3136
|
+
|
|
3137
|
+
# ${topic}
|
|
3138
|
+
|
|
3139
|
+
> Captured by proactive-curator on ${today2}. The user accepted this proposal; edit freely.
|
|
3140
|
+
|
|
3141
|
+
${summary}
|
|
3142
|
+
`;
|
|
3143
|
+
}
|
|
3144
|
+
function renderSectionBody(summary, now) {
|
|
3145
|
+
const ymd = formatYmd(now);
|
|
3146
|
+
return `
|
|
3147
|
+
_Appended ${ymd} by proactive-curator._
|
|
3148
|
+
|
|
3149
|
+
${summary}
|
|
3150
|
+
`;
|
|
3151
|
+
}
|
|
3152
|
+
function renderUpdateBody(summary, topic, reason, now) {
|
|
3153
|
+
const ymd = formatYmd(now);
|
|
3154
|
+
return `---
|
|
3155
|
+
type: note
|
|
3156
|
+
topic: ${topic}
|
|
3157
|
+
updated: ${ymd}
|
|
3158
|
+
---
|
|
3159
|
+
|
|
3160
|
+
# ${topic}
|
|
3161
|
+
|
|
3162
|
+
> Updated ${ymd} by proactive-curator. Reason: ${reason}
|
|
3163
|
+
|
|
3164
|
+
${summary}
|
|
3165
|
+
`;
|
|
3166
|
+
}
|
|
3167
|
+
function formatYmd(d2) {
|
|
2712
3168
|
const y2 = d2.getFullYear();
|
|
2713
3169
|
const m2 = String(d2.getMonth() + 1).padStart(2, "0");
|
|
2714
3170
|
const day = String(d2.getDate()).padStart(2, "0");
|
|
2715
3171
|
return `${y2}-${m2}-${day}`;
|
|
2716
3172
|
}
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
import { readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
|
|
2721
|
-
import { join as join15 } from "path";
|
|
2722
|
-
var TARGETS = [
|
|
2723
|
-
{
|
|
2724
|
-
dir: "_memory",
|
|
2725
|
-
title: "Memory",
|
|
2726
|
-
description: "\uC138\uC158 \uAC04 \uACF5\uC720\uB418\uB294 \uC601\uC18D \uBA54\uBAA8\uB9AC \uD56D\uBAA9\uB4E4. MEMORY.md\uB294 \uBCF8 \uB514\uB809\uD1A0\uB9AC\uC758 \uC6D0\uBCF8 \uC778\uB371\uC2A4\uC774\uBA70, \uBCF8 _INDEX.md\uB294 \uC0AC\uB78C\xB7Obsidian \uCE5C\uD654 \uC591\uC2DD\uC758 \uD30C\uC0DD\uBB3C\uC785\uB2C8\uB2E4.",
|
|
2727
|
-
privacy: "internal",
|
|
2728
|
-
recursive: false,
|
|
2729
|
-
skipPrefixes: [],
|
|
2730
|
-
skipFilenames: ["MEMORY.md"]
|
|
2731
|
-
},
|
|
2732
|
-
{
|
|
2733
|
-
dir: "til",
|
|
2734
|
-
title: "TIL",
|
|
2735
|
-
description: "\uB0A0\uC9DC\uBCC4 \uC791\uC5C5 \uAE30\uB85D. `YYYY/MM/YYYY-MM-DD-keyword.md` \uAD6C\uC870.",
|
|
2736
|
-
privacy: "internal",
|
|
2737
|
-
recursive: true,
|
|
2738
|
-
skipPrefixes: [],
|
|
2739
|
-
skipFilenames: []
|
|
2740
|
-
},
|
|
2741
|
-
{
|
|
2742
|
-
dir: "decision-log",
|
|
2743
|
-
title: "Decision Log",
|
|
2744
|
-
description: "\uAC1C\uC778 \uC758\uC0AC\uACB0\uC815 \uAE30\uB85D \u2014 \uC65C \uADF8\uAC78 \uACE8\uB790\uB294\uC9C0\uB97C \uB0A8\uAE41\uB2C8\uB2E4.",
|
|
2745
|
-
privacy: "personal",
|
|
2746
|
-
recursive: false,
|
|
2747
|
-
skipPrefixes: ["_TEMPLATE"],
|
|
2748
|
-
skipFilenames: []
|
|
2749
|
-
},
|
|
2750
|
-
{
|
|
2751
|
-
dir: "runbooks",
|
|
2752
|
-
title: "Runbooks",
|
|
2753
|
-
description: "\uC7A5\uC560 \uB300\uC751\xB7\uC815\uAE30 \uC815\uBE44 \uC808\uCC28. `last_tested`\uB85C \uAC31\uC2E0 \uAE30\uD55C \uCD94\uC801.",
|
|
2754
|
-
privacy: "internal",
|
|
2755
|
-
recursive: false,
|
|
2756
|
-
skipPrefixes: [],
|
|
2757
|
-
skipFilenames: []
|
|
2758
|
-
},
|
|
2759
|
-
{
|
|
2760
|
-
dir: "hubs",
|
|
2761
|
-
title: "Hubs",
|
|
2762
|
-
description: "\uC8FC\uC81C\uBCC4 \uC9C4\uC785\uC810 \u2014 \uAD00\uB828 \uC790\uB8CC\uB97C \uD55C \uACF3\uC5D0\uC11C \uBB36\uB294 \uC778\uB371\uC2A4 \uD398\uC774\uC9C0 \uBAA8\uC74C.",
|
|
2763
|
-
privacy: "internal",
|
|
2764
|
-
recursive: false,
|
|
2765
|
-
skipPrefixes: [],
|
|
2766
|
-
skipFilenames: []
|
|
2767
|
-
},
|
|
2768
|
-
{
|
|
2769
|
-
dir: "projects",
|
|
2770
|
-
title: "Projects",
|
|
2771
|
-
description: "\uC9C4\uD589 \uC911\xB7\uC644\uB8CC \uD504\uB85C\uC81D\uD2B8 \uBAA8\uC74C (Work\xB7Home). \uBCF8 _INDEX\uB294 \uD3C9\uD0C4 \uBAA9\uB85D\uC774\uBA70, \uD558\uC704 \uD504\uB85C\uC81D\uD2B8 \uD3F4\uB354\uBCC4 _INDEX\uAC00 \uB354 \uC720\uC6A9\uD560 \uC218 \uC788\uC74C (\uB2E4\uC74C \uC0AC\uC774\uD074 \uAC80\uD1A0).",
|
|
2772
|
-
privacy: "internal",
|
|
2773
|
-
recursive: true,
|
|
2774
|
-
skipPrefixes: [],
|
|
2775
|
-
skipFilenames: []
|
|
2776
|
-
},
|
|
2777
|
-
{
|
|
2778
|
-
dir: "reference",
|
|
2779
|
-
title: "Reference",
|
|
2780
|
-
description: "AI \uADDC\uCE59\xB7\uAE30\uC220 \uCE58\uD2B8\uC2DC\uD2B8\xB7\uC7A5\uBE44\xB7\uC2A4\uD0AC \uB4F1 \uB808\uD37C\uB7F0\uC2A4 \uC790\uB8CC (AI-Rules\xB7Games\xB7Gear\xB7Infra\xB7Org\xB7Skills\xB7Tech).",
|
|
2781
|
-
privacy: "internal",
|
|
2782
|
-
recursive: true,
|
|
2783
|
-
skipPrefixes: [],
|
|
2784
|
-
skipFilenames: []
|
|
2785
|
-
},
|
|
2786
|
-
{
|
|
2787
|
-
dir: "reports",
|
|
2788
|
-
title: "Reports",
|
|
2789
|
-
description: "\uC815\uAE30 \uAC74\uAC15\uAC80\uC9C4 \uB9AC\uD3EC\uD2B8 (Service-Health\xB7Homelab-Health). \uC2DC\uC810 \uC2A4\uB0C5\uC0F7 \uB204\uC801.",
|
|
2790
|
-
privacy: "internal",
|
|
2791
|
-
recursive: true,
|
|
2792
|
-
skipPrefixes: [],
|
|
2793
|
-
skipFilenames: []
|
|
2794
|
-
},
|
|
2795
|
-
{
|
|
2796
|
-
dir: "inbox",
|
|
2797
|
-
title: "Inbox",
|
|
2798
|
-
description: "\uBBF8\uBD84\uB958 \uC784\uC2DC \uBA54\uBAA8\xB7\uC544\uC774\uB514\uC5B4\xB7\uBC31\uB85C\uADF8. \uC815\uC2DD \uBD84\uB958 \uC2DC \uD574\uB2F9 \uCE74\uD14C\uACE0\uB9AC\uB85C \uC774\uB3D9.",
|
|
2799
|
-
privacy: "internal",
|
|
2800
|
-
recursive: true,
|
|
2801
|
-
skipPrefixes: [],
|
|
2802
|
-
skipFilenames: []
|
|
2803
|
-
},
|
|
2804
|
-
{
|
|
2805
|
-
dir: "_templates",
|
|
2806
|
-
title: "Templates",
|
|
2807
|
-
description: "\uBCF4\uACE0\uC6A9\xB7\uACF5\uC720\uC6A9 \uC790\uB8CC \uD15C\uD50C\uB9BF \uBAA8\uC74C (Sharing \uB4F1).",
|
|
2808
|
-
privacy: "internal",
|
|
2809
|
-
recursive: true,
|
|
2810
|
-
skipPrefixes: [],
|
|
2811
|
-
skipFilenames: []
|
|
3173
|
+
function targetPathFromAction(action) {
|
|
3174
|
+
if (action.kind === "create-folder" || action.kind === "create-file") {
|
|
3175
|
+
return joinDataPath(action.folderPath, action.filename);
|
|
2812
3176
|
}
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
3177
|
+
return joinDataPath(action.filePath);
|
|
3178
|
+
}
|
|
3179
|
+
function joinDataPath(...parts) {
|
|
3180
|
+
return parts.filter((p) => p.length > 0).map((p) => p.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "")).join("/");
|
|
3181
|
+
}
|
|
3182
|
+
async function applyAction(cwd, action) {
|
|
3183
|
+
const dataDir = join16(cwd, "data");
|
|
3184
|
+
switch (action.kind) {
|
|
3185
|
+
case "create-folder":
|
|
3186
|
+
case "create-file": {
|
|
3187
|
+
const folder = join16(dataDir, action.folderPath);
|
|
3188
|
+
await mkdir3(folder, { recursive: true });
|
|
3189
|
+
const file = join16(folder, action.filename);
|
|
3190
|
+
await writeFile6(file, action.body, "utf8");
|
|
3191
|
+
return file;
|
|
2821
3192
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
3193
|
+
case "append-section": {
|
|
3194
|
+
const file = join16(dataDir, action.filePath);
|
|
3195
|
+
await mkdir3(dirname2(file), { recursive: true });
|
|
3196
|
+
const existing = existsSync3(file) ? await readFile14(file, "utf8") : "";
|
|
3197
|
+
const newline = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
3198
|
+
const section = `${newline}
|
|
3199
|
+
## ${action.sectionHeader}
|
|
3200
|
+
${action.body}`;
|
|
3201
|
+
await appendFile2(file, section, "utf8");
|
|
3202
|
+
return file;
|
|
2828
3203
|
}
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
3204
|
+
case "update-file": {
|
|
3205
|
+
const file = join16(dataDir, action.filePath);
|
|
3206
|
+
await mkdir3(dirname2(file), { recursive: true });
|
|
3207
|
+
await writeFile6(file, action.body, "utf8");
|
|
3208
|
+
return file;
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
function nextActionHintFor(action) {
|
|
3213
|
+
if (action.kind === "append-section") {
|
|
3214
|
+
return `Section appended to ${action.filePath}. Open it to see the full thread of this topic.`;
|
|
3215
|
+
}
|
|
3216
|
+
if (action.kind === "update-file") {
|
|
3217
|
+
return `File updated at ${action.filePath}. The original content was replaced; check git history if you need to recover it.`;
|
|
3218
|
+
}
|
|
3219
|
+
return `New file at ${action.folderPath}/${action.filename}. Add cross-links from related docs as the topic grows.`;
|
|
3220
|
+
}
|
|
3221
|
+
async function scanTopicTree(cwd, maxEntries) {
|
|
3222
|
+
const dataDir = join16(cwd, "data");
|
|
3223
|
+
if (!existsSync3(dataDir)) {
|
|
3224
|
+
return { folders: [], truncated: false };
|
|
3225
|
+
}
|
|
3226
|
+
const folders = [];
|
|
3227
|
+
let counted = 0;
|
|
3228
|
+
let truncated = false;
|
|
3229
|
+
async function visit(absDir, relDir) {
|
|
3230
|
+
if (counted >= maxEntries) {
|
|
3231
|
+
truncated = true;
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
let entries;
|
|
3235
|
+
try {
|
|
3236
|
+
entries = await readdir12(absDir, { withFileTypes: true });
|
|
3237
|
+
} catch {
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
const files = [];
|
|
3241
|
+
const subdirs = [];
|
|
3242
|
+
for (const e of entries) {
|
|
3243
|
+
if (e.isDirectory()) {
|
|
3244
|
+
if (isReservedDir(e.name, relDir === ""))
|
|
3245
|
+
continue;
|
|
3246
|
+
subdirs.push(e.name);
|
|
3247
|
+
} else if (e.isFile() && e.name.endsWith(".md")) {
|
|
3248
|
+
if (e.name === "README.md" || e.name === "_INDEX.md" || e.name === "MEMORY.md")
|
|
3249
|
+
continue;
|
|
3250
|
+
const filePath = join16(absDir, e.name);
|
|
3251
|
+
let frontmatterTopic;
|
|
3252
|
+
let tags;
|
|
3253
|
+
try {
|
|
3254
|
+
const raw = await readFile14(filePath, "utf8");
|
|
3255
|
+
const parsed = parseFrontmatter(raw);
|
|
3256
|
+
if (typeof parsed.frontmatter.topic === "string") {
|
|
3257
|
+
frontmatterTopic = parsed.frontmatter.topic;
|
|
3258
|
+
}
|
|
3259
|
+
if (Array.isArray(parsed.frontmatter.tags)) {
|
|
3260
|
+
tags = parsed.frontmatter.tags.filter((t) => typeof t === "string");
|
|
3261
|
+
}
|
|
3262
|
+
} catch {
|
|
3263
|
+
}
|
|
3264
|
+
files.push({
|
|
3265
|
+
path: joinDataPath(relDir, e.name),
|
|
3266
|
+
filename: e.name,
|
|
3267
|
+
...frontmatterTopic ? { frontmatterTopic } : {},
|
|
3268
|
+
...tags ? { tags } : {}
|
|
3269
|
+
});
|
|
2835
3270
|
}
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
entries,
|
|
2845
|
-
privacy: t.privacy
|
|
2846
|
-
});
|
|
2847
|
-
const target = join15(dir, "_INDEX.md");
|
|
2848
|
-
let existing;
|
|
2849
|
-
try {
|
|
2850
|
-
existing = await readFile13(target, "utf8");
|
|
2851
|
-
} catch {
|
|
2852
|
-
existing = void 0;
|
|
3271
|
+
}
|
|
3272
|
+
if (files.length > 0 || subdirs.length === 0) {
|
|
3273
|
+
if (relDir !== "") {
|
|
3274
|
+
folders.push({ path: relDir, files });
|
|
3275
|
+
counted += 1 + files.length;
|
|
3276
|
+
} else if (files.length > 0) {
|
|
3277
|
+
folders.push({ path: ".", files });
|
|
3278
|
+
counted += 1 + files.length;
|
|
2853
3279
|
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
3280
|
+
}
|
|
3281
|
+
for (const d2 of subdirs) {
|
|
3282
|
+
if (counted >= maxEntries) {
|
|
3283
|
+
truncated = true;
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
3286
|
+
const childRel = joinDataPath(relDir, d2);
|
|
3287
|
+
await visit(join16(absDir, d2), childRel);
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
await visit(dataDir, "");
|
|
3291
|
+
return { folders, truncated };
|
|
3292
|
+
}
|
|
3293
|
+
function isReservedDir(name, atRoot) {
|
|
3294
|
+
if (atRoot && SYSTEM_META_DIRS.has(name))
|
|
3295
|
+
return true;
|
|
3296
|
+
if (name.startsWith("."))
|
|
3297
|
+
return true;
|
|
3298
|
+
if (atRoot && name.startsWith("_"))
|
|
3299
|
+
return true;
|
|
3300
|
+
return false;
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
// ../modules/proactive-curator/dist/hub-proposer.js
|
|
3304
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3305
|
+
import { mkdir as mkdir4, readFile as readFile15, readdir as readdir13, writeFile as writeFile7 } from "fs/promises";
|
|
3306
|
+
import { dirname as dirname3, join as join17 } from "path";
|
|
3307
|
+
var DEFAULT_CATEGORIES = [
|
|
3308
|
+
"worklog",
|
|
3309
|
+
"decision-log",
|
|
3310
|
+
"runbooks"
|
|
3311
|
+
];
|
|
3312
|
+
var HUB_DIR = "hubs";
|
|
3313
|
+
var MIN_KEYWORD_LENGTH = 3;
|
|
3314
|
+
var MAX_LLM_DOC_LIST = 30;
|
|
3315
|
+
var HubProposer = class {
|
|
3316
|
+
options;
|
|
3317
|
+
kind = "create-hub";
|
|
3318
|
+
constructor(options) {
|
|
3319
|
+
this.options = options;
|
|
3320
|
+
}
|
|
3321
|
+
async evaluate(input, ctx) {
|
|
3322
|
+
const categories = input.hint?.onlyCategories ?? this.options.scanCategories ?? DEFAULT_CATEGORIES;
|
|
3323
|
+
const docs = await scanDocs(ctx.cwd, categories);
|
|
3324
|
+
if (docs.length === 0)
|
|
3325
|
+
return null;
|
|
3326
|
+
const clusters = groupByTopic(docs);
|
|
3327
|
+
const candidate = pickCluster(clusters, this.options.weakSignalAt, ctx.cwd);
|
|
3328
|
+
if (!candidate)
|
|
3329
|
+
return null;
|
|
3330
|
+
const verdict = await askCoherence(ctx.llm, candidate, candidate.docs.length >= this.options.strongSignalAt);
|
|
3331
|
+
if (!verdict)
|
|
3332
|
+
return null;
|
|
3333
|
+
const slug = candidate.topic;
|
|
3334
|
+
const filename = `_HUB-${slug}.md`;
|
|
3335
|
+
const action = {
|
|
3336
|
+
kind: "create-file",
|
|
3337
|
+
folderPath: HUB_DIR,
|
|
3338
|
+
filename,
|
|
3339
|
+
body: renderHubBody(candidate, verdict.contextParagraph, ctx.now)
|
|
3340
|
+
};
|
|
3341
|
+
const targetPath = `${HUB_DIR}/${filename}`;
|
|
3342
|
+
const sourceDocs = candidate.docs.map((d2) => d2.path);
|
|
3343
|
+
const fingerprint = computeFingerprint({
|
|
3344
|
+
kind: "create-hub",
|
|
3345
|
+
topic: candidate.topic,
|
|
3346
|
+
sourceDocs,
|
|
3347
|
+
actionKind: action.kind,
|
|
3348
|
+
targetPath
|
|
3349
|
+
});
|
|
3350
|
+
if (ctx.declinedFingerprints.has(fingerprint))
|
|
3351
|
+
return null;
|
|
3352
|
+
const rationale = candidate.docs.length >= this.options.strongSignalAt ? `${candidate.docs.length} docs about "${candidate.topic}" \u2014 strong-signal cluster (above ${this.options.strongSignalAt}); LLM concurred.` : `${candidate.docs.length} docs about "${candidate.topic}" \u2014 weak-signal cluster (\u2265 ${this.options.weakSignalAt}); LLM affirmed cohesion.`;
|
|
3353
|
+
const declineExpiryDays = this.options.declineExpiryDays;
|
|
3354
|
+
return {
|
|
3355
|
+
kind: "create-hub",
|
|
3356
|
+
fingerprint,
|
|
3357
|
+
action,
|
|
3358
|
+
preview: renderPreview(candidate, verdict.contextParagraph),
|
|
3359
|
+
rationale,
|
|
3360
|
+
sourceRefs: sourceDocs,
|
|
3361
|
+
onAccept: async () => {
|
|
3362
|
+
const writtenPath = await applyHubCreate(ctx.cwd, action);
|
|
3363
|
+
await recordAcceptance(ctx.cwd, {
|
|
3364
|
+
kind: "create-hub",
|
|
3365
|
+
fingerprint,
|
|
3366
|
+
topic: candidate.topic,
|
|
3367
|
+
actionKind: action.kind,
|
|
3368
|
+
writtenPath,
|
|
3369
|
+
now: ctx.now
|
|
3370
|
+
});
|
|
3371
|
+
return {
|
|
3372
|
+
writtenPath,
|
|
3373
|
+
actionApplied: action.kind,
|
|
3374
|
+
nextActionHint: `Hub created at hubs/${filename}. Open it to review the cross-links; the source docs were not modified.`
|
|
3375
|
+
};
|
|
3376
|
+
},
|
|
3377
|
+
onDecline: async () => {
|
|
3378
|
+
await recordDecline(ctx.cwd, {
|
|
3379
|
+
kind: "create-hub",
|
|
3380
|
+
fingerprint,
|
|
3381
|
+
topic: candidate.topic,
|
|
3382
|
+
actionKind: action.kind,
|
|
3383
|
+
targetPath,
|
|
3384
|
+
sourceDocs,
|
|
3385
|
+
now: ctx.now,
|
|
3386
|
+
...declineExpiryDays !== void 0 ? { expiryDays: declineExpiryDays } : {}
|
|
2860
3387
|
});
|
|
3388
|
+
}
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3391
|
+
};
|
|
3392
|
+
async function scanDocs(cwd, categories) {
|
|
3393
|
+
const dataDir = join17(cwd, "data");
|
|
3394
|
+
if (!existsSync4(dataDir))
|
|
3395
|
+
return [];
|
|
3396
|
+
const out = [];
|
|
3397
|
+
for (const category of categories) {
|
|
3398
|
+
const abs = join17(dataDir, category);
|
|
3399
|
+
if (!existsSync4(abs))
|
|
3400
|
+
continue;
|
|
3401
|
+
await walk4(abs, category, out);
|
|
3402
|
+
}
|
|
3403
|
+
return out;
|
|
3404
|
+
}
|
|
3405
|
+
async function walk4(absDir, relPath, acc) {
|
|
3406
|
+
let entries;
|
|
3407
|
+
try {
|
|
3408
|
+
entries = await readdir13(absDir, { withFileTypes: true });
|
|
3409
|
+
} catch {
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
for (const e of entries) {
|
|
3413
|
+
if (e.isDirectory()) {
|
|
3414
|
+
if (e.name.startsWith(".") || e.name.startsWith("_"))
|
|
2861
3415
|
continue;
|
|
3416
|
+
await walk4(join17(absDir, e.name), `${relPath}/${e.name}`, acc);
|
|
3417
|
+
} else if (e.isFile() && e.name.endsWith(".md")) {
|
|
3418
|
+
if (e.name === "README.md" || e.name === "_INDEX.md" || e.name === "MEMORY.md")
|
|
3419
|
+
continue;
|
|
3420
|
+
if (e.name.startsWith("_TEMPLATE"))
|
|
3421
|
+
continue;
|
|
3422
|
+
const filePath = join17(absDir, e.name);
|
|
3423
|
+
let frontmatterTopic;
|
|
3424
|
+
let tags = [];
|
|
3425
|
+
try {
|
|
3426
|
+
const raw = await readFile15(filePath, "utf8");
|
|
3427
|
+
const parsed = parseFrontmatter(raw);
|
|
3428
|
+
if (typeof parsed.frontmatter.topic === "string") {
|
|
3429
|
+
frontmatterTopic = parsed.frontmatter.topic.trim();
|
|
3430
|
+
}
|
|
3431
|
+
if (Array.isArray(parsed.frontmatter.tags)) {
|
|
3432
|
+
tags = parsed.frontmatter.tags.filter((t) => typeof t === "string");
|
|
3433
|
+
}
|
|
3434
|
+
} catch {
|
|
2862
3435
|
}
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
3436
|
+
acc.push({
|
|
3437
|
+
path: `${relPath}/${e.name}`,
|
|
3438
|
+
filename: e.name,
|
|
3439
|
+
...frontmatterTopic ? { frontmatterTopic } : {},
|
|
3440
|
+
tags,
|
|
3441
|
+
filenameKeywords: extractFilenameKeywords(e.name)
|
|
2869
3442
|
});
|
|
2870
3443
|
}
|
|
2871
|
-
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
function extractFilenameKeywords(filename) {
|
|
3447
|
+
const stem = filename.replace(/\.md$/i, "");
|
|
3448
|
+
const withoutDate = stem.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
3449
|
+
return withoutDate.split(/[-_\s]+/).map((s) => s.toLowerCase()).filter((s) => s.length >= MIN_KEYWORD_LENGTH);
|
|
3450
|
+
}
|
|
3451
|
+
function groupByTopic(docs) {
|
|
3452
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3453
|
+
for (const doc of docs) {
|
|
3454
|
+
const topics = candidateTopicsFor(doc);
|
|
3455
|
+
for (const topic of topics) {
|
|
3456
|
+
const list = buckets.get(topic);
|
|
3457
|
+
if (list) {
|
|
3458
|
+
if (!list.some((d2) => d2.path === doc.path))
|
|
3459
|
+
list.push(doc);
|
|
3460
|
+
} else {
|
|
3461
|
+
buckets.set(topic, [doc]);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
const clusters = [];
|
|
3466
|
+
for (const [topic, list] of buckets) {
|
|
3467
|
+
clusters.push({ topic, docs: list });
|
|
3468
|
+
}
|
|
3469
|
+
clusters.sort((a, b2) => b2.docs.length - a.docs.length);
|
|
3470
|
+
return clusters;
|
|
3471
|
+
}
|
|
3472
|
+
function candidateTopicsFor(doc) {
|
|
3473
|
+
const topics = /* @__PURE__ */ new Set();
|
|
3474
|
+
if (doc.frontmatterTopic)
|
|
3475
|
+
topics.add(slugify(doc.frontmatterTopic));
|
|
3476
|
+
for (const tag of doc.tags) {
|
|
3477
|
+
const slug = slugify(tag);
|
|
3478
|
+
if (slug.length >= MIN_KEYWORD_LENGTH)
|
|
3479
|
+
topics.add(slug);
|
|
3480
|
+
}
|
|
3481
|
+
for (const kw of doc.filenameKeywords) {
|
|
3482
|
+
topics.add(kw);
|
|
3483
|
+
}
|
|
3484
|
+
return [...topics];
|
|
3485
|
+
}
|
|
3486
|
+
function slugify(s) {
|
|
3487
|
+
return s.trim().toLowerCase().replace(/\s+/g, "-");
|
|
3488
|
+
}
|
|
3489
|
+
function pickCluster(clusters, weakThreshold, cwd) {
|
|
3490
|
+
for (const c of clusters) {
|
|
3491
|
+
if (c.docs.length < weakThreshold)
|
|
3492
|
+
return null;
|
|
3493
|
+
const hubPath = join17(cwd, "data", HUB_DIR, `_HUB-${c.topic}.md`);
|
|
3494
|
+
if (existsSync4(hubPath))
|
|
3495
|
+
continue;
|
|
3496
|
+
return c;
|
|
3497
|
+
}
|
|
3498
|
+
return null;
|
|
3499
|
+
}
|
|
3500
|
+
async function askCoherence(llm, cluster, isStrongSignal) {
|
|
3501
|
+
const prompt = renderCoherencePrompt(cluster, isStrongSignal);
|
|
3502
|
+
const expected = {
|
|
3503
|
+
shape: "json",
|
|
3504
|
+
schema: {
|
|
3505
|
+
verdict: "yes|no",
|
|
3506
|
+
contextParagraph: "string (1-2 sentences, used in the hub body when verdict=yes)"
|
|
3507
|
+
}
|
|
3508
|
+
};
|
|
3509
|
+
const raw = await llm.ask(prompt, expected);
|
|
3510
|
+
return interpretCoherence(raw, isStrongSignal);
|
|
3511
|
+
}
|
|
3512
|
+
function renderCoherencePrompt(cluster, isStrongSignal) {
|
|
3513
|
+
const docList = cluster.docs.slice(0, MAX_LLM_DOC_LIST).map((d2) => ` - ${d2.path}`).join("\n");
|
|
3514
|
+
const truncationNote = cluster.docs.length > MAX_LLM_DOC_LIST ? `
|
|
3515
|
+
(... ${cluster.docs.length - MAX_LLM_DOC_LIST} more docs omitted for brevity)` : "";
|
|
3516
|
+
if (isStrongSignal) {
|
|
3517
|
+
return [
|
|
3518
|
+
`${cluster.docs.length} documents share the candidate topic "${cluster.topic}" \u2014 a strong-signal cluster.`,
|
|
3519
|
+
"",
|
|
3520
|
+
"Documents:",
|
|
3521
|
+
docList + truncationNote,
|
|
3522
|
+
"",
|
|
3523
|
+
"Default action is to propose creating a `_HUB-` page that cross-links them. Reject *only* if the docs are clearly incoherent \u2014 sharing a keyword but not a real subject. If the cluster has any reasonable cohesion, return verdict=yes.",
|
|
3524
|
+
"",
|
|
3525
|
+
`Return JSON: { "verdict": "yes" | "no", "contextParagraph": "1-2 sentences describing the topic for the hub body (required if verdict=yes)" }`
|
|
3526
|
+
].join("\n");
|
|
3527
|
+
}
|
|
3528
|
+
return [
|
|
3529
|
+
`${cluster.docs.length} documents share the candidate topic "${cluster.topic}" \u2014 a weak-signal cluster.`,
|
|
3530
|
+
"",
|
|
3531
|
+
"Documents:",
|
|
3532
|
+
docList + truncationNote,
|
|
3533
|
+
"",
|
|
3534
|
+
"Affirm verdict=yes only if the topic is clearly cross-cutting and the cluster has real cohesion (not a coincidence of keywords or tags). When uncertain, return verdict=no.",
|
|
3535
|
+
"",
|
|
3536
|
+
`Return JSON: { "verdict": "yes" | "no", "contextParagraph": "1-2 sentences describing the topic for the hub body (required if verdict=yes)" }`
|
|
3537
|
+
].join("\n");
|
|
3538
|
+
}
|
|
3539
|
+
function interpretCoherence(raw, isStrongSignal) {
|
|
3540
|
+
if (typeof raw !== "object" || raw === null)
|
|
3541
|
+
return null;
|
|
3542
|
+
const obj = raw;
|
|
3543
|
+
const verdict = obj.verdict === "yes" ? "yes" : obj.verdict === "no" ? "no" : null;
|
|
3544
|
+
if (verdict === null)
|
|
3545
|
+
return null;
|
|
3546
|
+
if (isStrongSignal) {
|
|
3547
|
+
if (verdict === "no")
|
|
3548
|
+
return null;
|
|
3549
|
+
const contextParagraph2 = typeof obj.contextParagraph === "string" ? obj.contextParagraph.trim() || void 0 : void 0;
|
|
3550
|
+
return contextParagraph2 ? { verdict: "yes", contextParagraph: contextParagraph2 } : { verdict: "yes" };
|
|
3551
|
+
}
|
|
3552
|
+
if (verdict !== "yes")
|
|
3553
|
+
return null;
|
|
3554
|
+
const contextParagraph = typeof obj.contextParagraph === "string" ? obj.contextParagraph.trim() : "";
|
|
3555
|
+
if (!contextParagraph)
|
|
3556
|
+
return null;
|
|
3557
|
+
return { verdict: "yes", contextParagraph };
|
|
3558
|
+
}
|
|
3559
|
+
function renderPreview(cluster, contextParagraph) {
|
|
3560
|
+
const lines = [
|
|
3561
|
+
`Proposed hub: hubs/_HUB-${cluster.topic}.md`,
|
|
3562
|
+
`Cluster size: ${cluster.docs.length} documents`
|
|
3563
|
+
];
|
|
3564
|
+
if (contextParagraph) {
|
|
3565
|
+
lines.push("", contextParagraph);
|
|
3566
|
+
}
|
|
3567
|
+
lines.push("", "Cross-linked docs:");
|
|
3568
|
+
for (const d2 of cluster.docs.slice(0, MAX_LLM_DOC_LIST)) {
|
|
3569
|
+
lines.push(` - ${d2.path}`);
|
|
3570
|
+
}
|
|
3571
|
+
if (cluster.docs.length > MAX_LLM_DOC_LIST) {
|
|
3572
|
+
lines.push(` ... ${cluster.docs.length - MAX_LLM_DOC_LIST} more`);
|
|
3573
|
+
}
|
|
3574
|
+
return lines.join("\n");
|
|
3575
|
+
}
|
|
3576
|
+
function renderHubBody(cluster, contextParagraph, now) {
|
|
3577
|
+
const ymd = formatYmd2(now);
|
|
3578
|
+
const links = cluster.docs.map((d2) => `- [[${stripMdExtension(d2.path)}]]`).join("\n");
|
|
3579
|
+
const context = contextParagraph ? `${contextParagraph}
|
|
3580
|
+
|
|
3581
|
+
` : "";
|
|
3582
|
+
return `---
|
|
3583
|
+
type: hub
|
|
3584
|
+
topic: ${cluster.topic}
|
|
3585
|
+
created: ${ymd}
|
|
3586
|
+
updated: ${ymd}
|
|
3587
|
+
tags: [hub, proactive-curator]
|
|
3588
|
+
---
|
|
3589
|
+
|
|
3590
|
+
# ${cluster.topic}
|
|
3591
|
+
|
|
3592
|
+
> Hub proposed by proactive-curator on ${ymd}. The user accepted this proposal; edit freely. The cross-linked source docs were not modified.
|
|
3593
|
+
|
|
3594
|
+
${context}## Cross-linked docs
|
|
3595
|
+
|
|
3596
|
+
${links}
|
|
3597
|
+
`;
|
|
3598
|
+
}
|
|
3599
|
+
function stripMdExtension(p) {
|
|
3600
|
+
return p.replace(/\.md$/i, "");
|
|
3601
|
+
}
|
|
3602
|
+
function formatYmd2(d2) {
|
|
3603
|
+
const y2 = d2.getFullYear();
|
|
3604
|
+
const m2 = String(d2.getMonth() + 1).padStart(2, "0");
|
|
3605
|
+
const day = String(d2.getDate()).padStart(2, "0");
|
|
3606
|
+
return `${y2}-${m2}-${day}`;
|
|
3607
|
+
}
|
|
3608
|
+
async function applyHubCreate(cwd, action) {
|
|
3609
|
+
const folder = join17(cwd, "data", action.folderPath);
|
|
3610
|
+
await mkdir4(folder, { recursive: true });
|
|
3611
|
+
const file = join17(folder, action.filename);
|
|
3612
|
+
await mkdir4(dirname3(file), { recursive: true });
|
|
3613
|
+
await writeFile7(file, action.body, "utf8");
|
|
3614
|
+
return file;
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
// ../modules/proactive-curator/dist/ambient-tracker.js
|
|
3618
|
+
var TurnBuffer = class {
|
|
3619
|
+
turns = [];
|
|
3620
|
+
maxTurns;
|
|
3621
|
+
estimator;
|
|
3622
|
+
constructor(options) {
|
|
3623
|
+
this.maxTurns = options?.maxTurns ?? 50;
|
|
3624
|
+
this.estimator = options?.estimator ?? defaultTokenEstimator;
|
|
3625
|
+
}
|
|
3626
|
+
/** Append a turn, evicting the oldest entry if the buffer is full. */
|
|
3627
|
+
add(turn) {
|
|
3628
|
+
this.turns.push(turn);
|
|
3629
|
+
while (this.turns.length > this.maxTurns) {
|
|
3630
|
+
this.turns.shift();
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
/** Return the most recent `n` turns (or fewer if the buffer has fewer). */
|
|
3634
|
+
recent(n) {
|
|
3635
|
+
if (n >= this.turns.length)
|
|
3636
|
+
return [...this.turns];
|
|
3637
|
+
return this.turns.slice(-n);
|
|
3638
|
+
}
|
|
3639
|
+
/** Number of turns currently buffered. */
|
|
3640
|
+
get length() {
|
|
3641
|
+
return this.turns.length;
|
|
3642
|
+
}
|
|
3643
|
+
/** Estimated total token count over the entire buffer. */
|
|
3644
|
+
tokenCount() {
|
|
3645
|
+
let total = 0;
|
|
3646
|
+
for (const t of this.turns)
|
|
3647
|
+
total += this.estimator(t);
|
|
3648
|
+
return total;
|
|
3649
|
+
}
|
|
3650
|
+
/** Reset the buffer — typically called at session-end. */
|
|
3651
|
+
clear() {
|
|
3652
|
+
this.turns.length = 0;
|
|
3653
|
+
}
|
|
3654
|
+
};
|
|
3655
|
+
var AmbientBackpressure = class {
|
|
3656
|
+
declineStreak = 0;
|
|
3657
|
+
maxStreak;
|
|
3658
|
+
constructor(options) {
|
|
3659
|
+
this.maxStreak = options?.maxStreak ?? 3;
|
|
3660
|
+
}
|
|
3661
|
+
/** Record a user decline. Counts toward backpressure. */
|
|
3662
|
+
recordDecline() {
|
|
3663
|
+
this.declineStreak++;
|
|
3664
|
+
}
|
|
3665
|
+
/** Record a user acceptance. Clears the streak. */
|
|
3666
|
+
recordAccept() {
|
|
3667
|
+
this.declineStreak = 0;
|
|
3668
|
+
}
|
|
3669
|
+
/** Explicit reset (e.g. user runs `/curate` after backpressure activated). */
|
|
3670
|
+
reset() {
|
|
3671
|
+
this.declineStreak = 0;
|
|
3672
|
+
}
|
|
3673
|
+
/** True when the streak has reached the maximum and ambient should pause. */
|
|
3674
|
+
isActive() {
|
|
3675
|
+
return this.declineStreak >= this.maxStreak;
|
|
3676
|
+
}
|
|
3677
|
+
/** Current decline streak — exposed for diagnostics. */
|
|
3678
|
+
get streak() {
|
|
3679
|
+
return this.declineStreak;
|
|
2872
3680
|
}
|
|
2873
3681
|
};
|
|
3682
|
+
function defaultTokenEstimator(turn) {
|
|
3683
|
+
return Math.ceil(turn.content.length / 4);
|
|
3684
|
+
}
|
|
2874
3685
|
|
|
2875
|
-
// ../
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
var
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
3686
|
+
// ../modules/proactive-curator/dist/ambient-recaller.js
|
|
3687
|
+
var DEFAULT_MIN_SCORE = 0.5;
|
|
3688
|
+
var DEFAULT_MAX_SUGGESTIONS = 1;
|
|
3689
|
+
var DEFAULT_MIN_QUERY_CHARS = 12;
|
|
3690
|
+
var AmbientRecaller = class {
|
|
3691
|
+
recall;
|
|
3692
|
+
minScore;
|
|
3693
|
+
maxSuggestions;
|
|
3694
|
+
minQueryChars;
|
|
3695
|
+
candidateK;
|
|
3696
|
+
backpressure;
|
|
3697
|
+
surfaced = /* @__PURE__ */ new Set();
|
|
3698
|
+
constructor(options) {
|
|
3699
|
+
this.recall = options.recall;
|
|
3700
|
+
this.minScore = options.minScore ?? DEFAULT_MIN_SCORE;
|
|
3701
|
+
this.maxSuggestions = options.maxSuggestions ?? DEFAULT_MAX_SUGGESTIONS;
|
|
3702
|
+
this.minQueryChars = options.minQueryChars ?? DEFAULT_MIN_QUERY_CHARS;
|
|
3703
|
+
this.candidateK = options.candidateK ?? this.maxSuggestions + 4;
|
|
3704
|
+
this.backpressure = options.backpressure ?? new AmbientBackpressure();
|
|
3705
|
+
}
|
|
3706
|
+
/**
|
|
3707
|
+
* Consider surfacing a memory given the current conversation. Pass an
|
|
3708
|
+
* explicit `query` (the host knows what the user is referencing) or recent
|
|
3709
|
+
* `turns` to derive one from. Returns the gated suggestions plus diagnostics.
|
|
3710
|
+
*
|
|
3711
|
+
* Does not catch errors from the injected engine — a failing embedder
|
|
3712
|
+
* should surface to the host (which decides whether to swallow it for the
|
|
3713
|
+
* turn), not be masked here.
|
|
3714
|
+
*/
|
|
3715
|
+
async consider(input) {
|
|
3716
|
+
if (this.backpressure.isActive()) {
|
|
3717
|
+
return emptyResult(true, null);
|
|
3718
|
+
}
|
|
3719
|
+
const query = (input.query ?? deriveQueryFromTurns(input.turns ?? [])).trim();
|
|
3720
|
+
if (query.length < this.minQueryChars) {
|
|
3721
|
+
return emptyResult(false, query.length === 0 ? null : query);
|
|
3722
|
+
}
|
|
3723
|
+
const { hits } = await this.recall(query, { k: this.candidateK });
|
|
3724
|
+
let belowThreshold = 0;
|
|
3725
|
+
let deduped = 0;
|
|
3726
|
+
const suggestions = [];
|
|
3727
|
+
for (const hit of hits) {
|
|
3728
|
+
if (hit.score < this.minScore) {
|
|
3729
|
+
belowThreshold++;
|
|
2892
3730
|
continue;
|
|
2893
3731
|
}
|
|
2894
|
-
|
|
3732
|
+
if (this.surfaced.has(hit.id)) {
|
|
3733
|
+
deduped++;
|
|
3734
|
+
continue;
|
|
3735
|
+
}
|
|
3736
|
+
suggestions.push({ hit, score: hit.score });
|
|
3737
|
+
if (suggestions.length >= this.maxSuggestions)
|
|
3738
|
+
break;
|
|
3739
|
+
}
|
|
3740
|
+
for (const s of suggestions)
|
|
3741
|
+
this.surfaced.add(s.hit.id);
|
|
3742
|
+
return {
|
|
3743
|
+
suggestions,
|
|
3744
|
+
suppressed: false,
|
|
3745
|
+
query,
|
|
3746
|
+
considered: hits.length,
|
|
3747
|
+
belowThreshold,
|
|
3748
|
+
deduped
|
|
3749
|
+
};
|
|
3750
|
+
}
|
|
3751
|
+
/** The user engaged with a surfaced suggestion — clears the decline streak. */
|
|
3752
|
+
recordAccept() {
|
|
3753
|
+
this.backpressure.recordAccept();
|
|
3754
|
+
}
|
|
3755
|
+
/** The user dismissed a surfaced suggestion — counts toward backpressure. */
|
|
3756
|
+
recordDecline() {
|
|
3757
|
+
this.backpressure.recordDecline();
|
|
3758
|
+
}
|
|
3759
|
+
/** True when backpressure has silenced ambient surfacing for the session. */
|
|
3760
|
+
get suppressed() {
|
|
3761
|
+
return this.backpressure.isActive();
|
|
3762
|
+
}
|
|
3763
|
+
/**
|
|
3764
|
+
* Re-engage after backpressure (the user explicitly asks for suggestions
|
|
3765
|
+
* again, e.g. via `/curate`). Clears the decline streak AND the dedup set
|
|
3766
|
+
* so previously surfaced hits can be offered again.
|
|
3767
|
+
*/
|
|
3768
|
+
reset() {
|
|
3769
|
+
this.backpressure.reset();
|
|
3770
|
+
this.surfaced.clear();
|
|
3771
|
+
}
|
|
3772
|
+
};
|
|
3773
|
+
function deriveQueryFromTurns(turns) {
|
|
3774
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
3775
|
+
if (turns[i].role === "user")
|
|
3776
|
+
return turns[i].content;
|
|
3777
|
+
}
|
|
3778
|
+
return turns.length > 0 ? turns[turns.length - 1].content : "";
|
|
3779
|
+
}
|
|
3780
|
+
function emptyResult(suppressed, query) {
|
|
3781
|
+
return { suggestions: [], suppressed, query, considered: 0, belowThreshold: 0, deduped: 0 };
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
// ../modules/proactive-curator/dist/adapters/shared.js
|
|
3785
|
+
var LLMJudgeError = class extends Error {
|
|
3786
|
+
raw;
|
|
3787
|
+
constructor(message, raw) {
|
|
3788
|
+
super(message);
|
|
3789
|
+
this.raw = raw;
|
|
3790
|
+
this.name = "LLMJudgeError";
|
|
3791
|
+
}
|
|
3792
|
+
};
|
|
3793
|
+
function frameForJudge(prompt, expected) {
|
|
3794
|
+
const tail = expected.shape === "json" ? '\n\nReturn ONLY the JSON object described above. No prose, no markdown code fences, no explanation around it. If you cannot answer, return the JSON with verdict="no" (for capture decisions) or actionKind="create-file" with a brief rationale (for placement decisions).' : "\n\nReturn ONLY the answer string. No prose preamble, no markdown formatting around it.";
|
|
3795
|
+
return prompt + tail;
|
|
3796
|
+
}
|
|
3797
|
+
function parseJudgeResponse(raw, expected, makeError) {
|
|
3798
|
+
if (typeof raw !== "string") {
|
|
3799
|
+
throw makeError("Host LLM returned a non-string response.");
|
|
3800
|
+
}
|
|
3801
|
+
const stripped = raw.trim();
|
|
3802
|
+
if (expected.shape === "string") {
|
|
3803
|
+
return stripped;
|
|
3804
|
+
}
|
|
3805
|
+
const jsonPayload = extractJsonPayload(stripped);
|
|
3806
|
+
try {
|
|
3807
|
+
return JSON.parse(jsonPayload);
|
|
3808
|
+
} catch (e) {
|
|
3809
|
+
throw makeError(`Host LLM response was not valid JSON: ${e.message}`, raw);
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
function extractJsonPayload(s) {
|
|
3813
|
+
const fenced = s.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
3814
|
+
if (fenced)
|
|
3815
|
+
return fenced[1].trim();
|
|
3816
|
+
const firstBrace = s.indexOf("{");
|
|
3817
|
+
const lastBrace = s.lastIndexOf("}");
|
|
3818
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
3819
|
+
return s.slice(firstBrace, lastBrace + 1);
|
|
3820
|
+
}
|
|
3821
|
+
return s;
|
|
3822
|
+
}
|
|
3823
|
+
var InjectedLLMJudge = class {
|
|
3824
|
+
invoker;
|
|
3825
|
+
constructor(invoker) {
|
|
3826
|
+
this.invoker = invoker;
|
|
3827
|
+
}
|
|
3828
|
+
/** Host adapters override to raise a host-named error subclass. */
|
|
3829
|
+
makeError(message, raw) {
|
|
3830
|
+
return new LLMJudgeError(message, raw);
|
|
3831
|
+
}
|
|
3832
|
+
async ask(prompt, expected) {
|
|
3833
|
+
const framed = frameForJudge(prompt, expected);
|
|
3834
|
+
const raw = await this.invoker({ prompt: framed });
|
|
3835
|
+
return parseJudgeResponse(raw, expected, (m2, r) => this.makeError(m2, r));
|
|
3836
|
+
}
|
|
3837
|
+
};
|
|
3838
|
+
|
|
3839
|
+
// ../modules/proactive-curator/dist/adapters/claude-code.js
|
|
3840
|
+
var ClaudeCodeLLMJudgeError = class extends LLMJudgeError {
|
|
3841
|
+
constructor(message, raw) {
|
|
3842
|
+
super(message, raw);
|
|
3843
|
+
this.name = "ClaudeCodeLLMJudgeError";
|
|
3844
|
+
}
|
|
3845
|
+
};
|
|
3846
|
+
var ClaudeCodeLLMJudge = class extends InjectedLLMJudge {
|
|
3847
|
+
host = "claude-code";
|
|
3848
|
+
makeError(message, raw) {
|
|
3849
|
+
return new ClaudeCodeLLMJudgeError(message, raw);
|
|
3850
|
+
}
|
|
3851
|
+
};
|
|
3852
|
+
|
|
3853
|
+
// ../modules/proactive-curator/dist/adapters/codex.js
|
|
3854
|
+
var CodexLLMJudgeError = class extends LLMJudgeError {
|
|
3855
|
+
constructor(message, raw) {
|
|
3856
|
+
super(message, raw);
|
|
3857
|
+
this.name = "CodexLLMJudgeError";
|
|
3858
|
+
}
|
|
3859
|
+
};
|
|
3860
|
+
var CodexLLMJudge = class extends InjectedLLMJudge {
|
|
3861
|
+
host = "codex";
|
|
3862
|
+
makeError(message, raw) {
|
|
3863
|
+
return new CodexLLMJudgeError(message, raw);
|
|
3864
|
+
}
|
|
3865
|
+
};
|
|
3866
|
+
|
|
3867
|
+
// ../modules/proactive-curator/dist/adapters/gemini.js
|
|
3868
|
+
var GeminiLLMJudgeError = class extends LLMJudgeError {
|
|
3869
|
+
constructor(message, raw) {
|
|
3870
|
+
super(message, raw);
|
|
3871
|
+
this.name = "GeminiLLMJudgeError";
|
|
3872
|
+
}
|
|
3873
|
+
};
|
|
3874
|
+
var GeminiLLMJudge = class extends InjectedLLMJudge {
|
|
3875
|
+
host = "gemini";
|
|
3876
|
+
makeError(message, raw) {
|
|
3877
|
+
return new GeminiLLMJudgeError(message, raw);
|
|
3878
|
+
}
|
|
3879
|
+
};
|
|
3880
|
+
|
|
3881
|
+
// ../modules/proactive-curator/dist/adapters/claude-desktop.js
|
|
3882
|
+
var ClaudeDesktopLLMJudgeError = class extends LLMJudgeError {
|
|
3883
|
+
constructor(message, raw) {
|
|
3884
|
+
super(message, raw);
|
|
3885
|
+
this.name = "ClaudeDesktopLLMJudgeError";
|
|
3886
|
+
}
|
|
3887
|
+
};
|
|
3888
|
+
var ClaudeDesktopLLMJudge = class extends InjectedLLMJudge {
|
|
3889
|
+
host = "claude-desktop";
|
|
3890
|
+
makeError(message, raw) {
|
|
3891
|
+
return new ClaudeDesktopLLMJudgeError(message, raw);
|
|
3892
|
+
}
|
|
3893
|
+
};
|
|
3894
|
+
|
|
3895
|
+
// ../plugins/session-rituals/dist/index.js
|
|
3896
|
+
var dist_exports14 = {};
|
|
3897
|
+
__export(dist_exports14, {
|
|
3898
|
+
SESSION_END_COMMAND: () => SESSION_END_COMMAND,
|
|
3899
|
+
SESSION_START_COMMAND: () => SESSION_START_COMMAND,
|
|
3900
|
+
agendaCommand: () => agendaCommand,
|
|
3901
|
+
catchUpSessions: () => catchUpSessions,
|
|
3902
|
+
collectAgenda: () => collectAgenda,
|
|
3903
|
+
collectSessionStartReport: () => collectSessionStartReport,
|
|
3904
|
+
createAmbientRecaller: () => createAmbientRecaller,
|
|
3905
|
+
createRitualRegistry: () => createRitualRegistry,
|
|
3906
|
+
curateCommand: () => curateCommand,
|
|
3907
|
+
decisionCommand: () => decisionCommand,
|
|
3908
|
+
detectWorklogGaps: () => detectWorklogGaps,
|
|
3909
|
+
ensureVortexHooks: () => ensureVortexHooks,
|
|
3910
|
+
ensureWorklogEntry: () => ensureWorklogEntry,
|
|
3911
|
+
extractNextUp: () => extractNextUp,
|
|
3912
|
+
extractOpenTasks: () => extractOpenTasks,
|
|
3913
|
+
logCommand: () => logCommand,
|
|
3914
|
+
parseSettings: () => parseSettings,
|
|
3915
|
+
recallCommand: () => recallCommand,
|
|
3916
|
+
reindexCommand: () => reindexCommand,
|
|
3917
|
+
renderAgenda: () => renderAgenda,
|
|
3918
|
+
renderSessionStartReport: () => renderSessionStartReport,
|
|
3919
|
+
serializeSettings: () => serializeSettings,
|
|
3920
|
+
sessionStartCommand: () => sessionStartCommand,
|
|
3921
|
+
vortexCommand: () => vortexCommand
|
|
3922
|
+
});
|
|
3923
|
+
|
|
3924
|
+
// ../plugins/session-rituals/dist/commands/curate.js
|
|
3925
|
+
function parseCurateArgs(input) {
|
|
3926
|
+
const args = {};
|
|
3927
|
+
for (const t of input.split(/\s+/).filter(Boolean)) {
|
|
3928
|
+
if (t === "--insight")
|
|
3929
|
+
args.insightOnly = true;
|
|
3930
|
+
else if (t === "--hub")
|
|
3931
|
+
args.hubOnly = true;
|
|
3932
|
+
else if (t === "--reset-declined")
|
|
3933
|
+
args.resetDeclined = true;
|
|
3934
|
+
}
|
|
3935
|
+
return args;
|
|
3936
|
+
}
|
|
3937
|
+
function curateCommand(options) {
|
|
3938
|
+
const insightProposer = options.insightProposer ?? new InsightProposer({
|
|
3939
|
+
minTokensAccumulated: 2e3,
|
|
3940
|
+
minTurnsBeforeFirstCheck: 6
|
|
3941
|
+
});
|
|
3942
|
+
const hubProposer = options.hubProposer ?? new HubProposer({
|
|
3943
|
+
weakSignalAt: 3,
|
|
3944
|
+
strongSignalAt: 5
|
|
3945
|
+
});
|
|
3946
|
+
return {
|
|
3947
|
+
name: "curate",
|
|
3948
|
+
description: "Run proactive-curator proposers and surface any proposals. Flags: --insight | --hub | --reset-declined.",
|
|
3949
|
+
args: [
|
|
3950
|
+
{
|
|
3951
|
+
name: "flag",
|
|
3952
|
+
description: "Optional: --insight, --hub, or --reset-declined.",
|
|
3953
|
+
required: false
|
|
3954
|
+
}
|
|
3955
|
+
],
|
|
3956
|
+
handler: async (input) => {
|
|
3957
|
+
const args = parseCurateArgs(input.rest);
|
|
3958
|
+
const { repoRoot } = input.context;
|
|
3959
|
+
const now = /* @__PURE__ */ new Date();
|
|
3960
|
+
if (args.resetDeclined) {
|
|
3961
|
+
await resetDeclined(repoRoot);
|
|
3962
|
+
return {
|
|
3963
|
+
subcommand: "curate",
|
|
3964
|
+
status: "reset-declined",
|
|
3965
|
+
proposals: [],
|
|
3966
|
+
skipped: { insight: false, hub: false },
|
|
3967
|
+
nextActions: [
|
|
3968
|
+
"Decline list cleared. Both insight and hub proposers will re-evaluate previously declined topics on the next /curate."
|
|
3969
|
+
]
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
const runInsight = !args.hubOnly;
|
|
3973
|
+
const runHub = !args.insightOnly;
|
|
3974
|
+
const declinedFingerprints = await loadDeclinedFingerprints(repoRoot, now);
|
|
3975
|
+
const proposals = [];
|
|
3976
|
+
let insightSkipped = !runInsight;
|
|
3977
|
+
let hubSkipped = !runHub;
|
|
3978
|
+
if (runInsight) {
|
|
3979
|
+
const insightInput = options.insightInputProvider?.() ?? null;
|
|
3980
|
+
if (!insightInput) {
|
|
3981
|
+
insightSkipped = true;
|
|
3982
|
+
} else {
|
|
3983
|
+
const p = await insightProposer.evaluate(insightInput, {
|
|
3984
|
+
cwd: repoRoot,
|
|
3985
|
+
declinedFingerprints,
|
|
3986
|
+
llm: options.llm,
|
|
3987
|
+
now
|
|
3988
|
+
});
|
|
3989
|
+
if (p)
|
|
3990
|
+
proposals.push(p);
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
if (runHub) {
|
|
3994
|
+
const p = await hubProposer.evaluate({}, {
|
|
3995
|
+
cwd: repoRoot,
|
|
3996
|
+
declinedFingerprints,
|
|
3997
|
+
llm: options.llm,
|
|
3998
|
+
now
|
|
3999
|
+
});
|
|
4000
|
+
if (p)
|
|
4001
|
+
proposals.push(p);
|
|
4002
|
+
}
|
|
4003
|
+
const nextActions = [];
|
|
4004
|
+
if (proposals.length === 0) {
|
|
4005
|
+
nextActions.push("No proposals to surface right now.");
|
|
4006
|
+
if (insightSkipped && hubSkipped) {
|
|
4007
|
+
nextActions.push("Both proposers were skipped \u2014 run /curate without flags to enable both.");
|
|
4008
|
+
} else if (insightSkipped && runInsight) {
|
|
4009
|
+
nextActions.push("Insight proposer was skipped because no insightInputProvider was registered with this plugin.");
|
|
4010
|
+
}
|
|
4011
|
+
} else {
|
|
4012
|
+
nextActions.push(`${proposals.length} proposal${proposals.length === 1 ? "" : "s"} ready. Review each preview and call onAccept() or onDecline() to apply or dismiss.`);
|
|
4013
|
+
}
|
|
4014
|
+
return {
|
|
4015
|
+
subcommand: "curate",
|
|
4016
|
+
status: "ok",
|
|
4017
|
+
proposals,
|
|
4018
|
+
skipped: { insight: insightSkipped, hub: hubSkipped },
|
|
4019
|
+
nextActions
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
};
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
// ../plugins/session-rituals/dist/commands/recall.js
|
|
4026
|
+
import { join as join18 } from "path";
|
|
4027
|
+
function parseRecallArgs(rest, defaultK) {
|
|
4028
|
+
const tokens = rest.split(/\s+/).filter(Boolean);
|
|
4029
|
+
const out = { k: defaultK, query: "" };
|
|
4030
|
+
const queryParts = [];
|
|
4031
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
4032
|
+
const t = tokens[i];
|
|
4033
|
+
if (t === "--k" && i + 1 < tokens.length) {
|
|
4034
|
+
const n = Number(tokens[++i]);
|
|
4035
|
+
if (Number.isFinite(n) && n > 0)
|
|
4036
|
+
out.k = Math.floor(n);
|
|
4037
|
+
} else if (t.startsWith("--k=")) {
|
|
4038
|
+
const n = Number(t.slice("--k=".length));
|
|
4039
|
+
if (Number.isFinite(n) && n > 0)
|
|
4040
|
+
out.k = Math.floor(n);
|
|
4041
|
+
} else if (t === "--source" && i + 1 < tokens.length) {
|
|
4042
|
+
out.source = tokens[++i];
|
|
4043
|
+
} else if (t.startsWith("--source=")) {
|
|
4044
|
+
out.source = t.slice("--source=".length);
|
|
4045
|
+
} else if (t === "--no-filter") {
|
|
4046
|
+
out.noHardFilter = true;
|
|
4047
|
+
} else {
|
|
4048
|
+
queryParts.push(t);
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
out.query = queryParts.join(" ");
|
|
4052
|
+
return out;
|
|
4053
|
+
}
|
|
4054
|
+
function defaultDbPath(ctx) {
|
|
4055
|
+
return join18(ctx.dataDir, "_indexes", "memory.sqlite");
|
|
4056
|
+
}
|
|
4057
|
+
function recallCommand(options) {
|
|
4058
|
+
const defaultK = options.defaultK ?? 5;
|
|
4059
|
+
const resolveDb = options.dbPath ?? defaultDbPath;
|
|
4060
|
+
return {
|
|
4061
|
+
name: "recall",
|
|
4062
|
+
description: "Hybrid semantic search over memories. Usage: /recall <query> [--k N] [--source memory] [--no-filter].",
|
|
4063
|
+
args: [{ name: "query", description: "Natural-language query.", required: true }],
|
|
4064
|
+
handler: async (input) => {
|
|
4065
|
+
const { sqlite, vector, recall: recallEngine, sessionArchive } = await import("@vortex-os/memory-extended");
|
|
4066
|
+
const args = parseRecallArgs(input.rest, defaultK);
|
|
4067
|
+
const dbPath = resolveDb(input.context);
|
|
4068
|
+
if (args.query.trim().length === 0) {
|
|
4069
|
+
return {
|
|
4070
|
+
query: "",
|
|
4071
|
+
intent: { filters: {}, semanticText: "", notes: ["empty query"] },
|
|
4072
|
+
stage: { appliedFilters: {}, hardFilterCandidates: 0, hardFilterDropped: false, corpusSize: 0 },
|
|
4073
|
+
hits: []
|
|
4074
|
+
};
|
|
4075
|
+
}
|
|
4076
|
+
const sqlStore = new sqlite.MemorySqliteStore(dbPath);
|
|
4077
|
+
const vecStore = new vector.MemoryVectorStore({ db: dbPath });
|
|
4078
|
+
try {
|
|
4079
|
+
const archive = new sessionArchive.SessionArchiveStore(input.context.dataDir);
|
|
4080
|
+
try {
|
|
4081
|
+
await vecStore.rebuildSessions(archive, options.embed, { onlyMissing: true });
|
|
4082
|
+
} finally {
|
|
4083
|
+
archive.close();
|
|
4084
|
+
}
|
|
4085
|
+
} catch {
|
|
4086
|
+
}
|
|
4087
|
+
const chunkStore = new vector.SessionChunkStore(dbPath);
|
|
4088
|
+
try {
|
|
4089
|
+
return await recallEngine.recall({ query: args.query, k: args.k, source: args.source, noHardFilter: args.noHardFilter }, { sqlite: sqlStore, vector: vecStore, embed: options.embed, sessionChunks: chunkStore });
|
|
4090
|
+
} finally {
|
|
4091
|
+
chunkStore.close();
|
|
4092
|
+
vecStore.close();
|
|
4093
|
+
sqlStore.close();
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
};
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
// ../plugins/session-rituals/dist/commands/decision.js
|
|
4100
|
+
import { writeFile as writeFile8 } from "fs/promises";
|
|
4101
|
+
import { join as join19 } from "path";
|
|
4102
|
+
import { existsSync as existsSync5 } from "fs";
|
|
4103
|
+
var decisionCommand = {
|
|
4104
|
+
name: "decision",
|
|
4105
|
+
description: "Create a new Decision Log entry from the canonical template at `data/decision-log/<today>-<slug>.md`. Refuses to overwrite an existing file.",
|
|
4106
|
+
args: [
|
|
4107
|
+
{ name: "slug", description: "Kebab-style identifier used in the filename.", required: true },
|
|
4108
|
+
{ name: "title", description: "One-line decision title (rest of the input).", required: true }
|
|
4109
|
+
],
|
|
4110
|
+
handler: async (input) => {
|
|
4111
|
+
const slug = input.args.slug;
|
|
4112
|
+
if (!slug) {
|
|
4113
|
+
throw new Error("`/decision` requires a slug argument.");
|
|
4114
|
+
}
|
|
4115
|
+
const title = extractTitle2(input.rest, slug);
|
|
4116
|
+
if (!title) {
|
|
4117
|
+
throw new Error("`/decision` requires a title after the slug.");
|
|
4118
|
+
}
|
|
4119
|
+
const date = todayIso();
|
|
4120
|
+
const dir = join19(input.context.dataDir, "decision-log");
|
|
4121
|
+
const store = new DecisionStore(dir);
|
|
4122
|
+
const path = store.pathFor(date, slug);
|
|
4123
|
+
if (existsSync5(path)) {
|
|
4124
|
+
throw new Error(`Refusing to overwrite existing entry: ${path}`);
|
|
4125
|
+
}
|
|
4126
|
+
const body = renderTemplate({ date, slug, title });
|
|
4127
|
+
await writeFile8(path, body, "utf8");
|
|
4128
|
+
return { path, date, slug };
|
|
4129
|
+
}
|
|
4130
|
+
};
|
|
4131
|
+
function extractTitle2(rest, slug) {
|
|
4132
|
+
const trimmed = rest.trim();
|
|
4133
|
+
if (trimmed.startsWith(slug)) {
|
|
4134
|
+
return trimmed.slice(slug.length).trim();
|
|
4135
|
+
}
|
|
4136
|
+
return trimmed;
|
|
4137
|
+
}
|
|
4138
|
+
function todayIso() {
|
|
4139
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
4140
|
+
const y2 = d2.getFullYear();
|
|
4141
|
+
const m2 = String(d2.getMonth() + 1).padStart(2, "0");
|
|
4142
|
+
const day = String(d2.getDate()).padStart(2, "0");
|
|
4143
|
+
return `${y2}-${m2}-${day}`;
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
// ../plugins/session-rituals/dist/commands/reindex.js
|
|
4147
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4148
|
+
import { readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
|
|
4149
|
+
import { join as join20 } from "path";
|
|
4150
|
+
var TARGETS = [
|
|
4151
|
+
{
|
|
4152
|
+
dir: "_memory",
|
|
4153
|
+
title: "Memory",
|
|
4154
|
+
description: "\uC138\uC158 \uAC04 \uACF5\uC720\uB418\uB294 \uC601\uC18D \uBA54\uBAA8\uB9AC \uD56D\uBAA9\uB4E4. MEMORY.md\uB294 \uBCF8 \uB514\uB809\uD1A0\uB9AC\uC758 \uC6D0\uBCF8 \uC778\uB371\uC2A4\uC774\uBA70, \uBCF8 _INDEX.md\uB294 \uC0AC\uB78C\xB7Obsidian \uCE5C\uD654 \uC591\uC2DD\uC758 \uD30C\uC0DD\uBB3C\uC785\uB2C8\uB2E4.",
|
|
4155
|
+
privacy: "internal",
|
|
4156
|
+
recursive: false,
|
|
4157
|
+
skipPrefixes: [],
|
|
4158
|
+
skipFilenames: ["MEMORY.md"]
|
|
4159
|
+
},
|
|
4160
|
+
{
|
|
4161
|
+
dir: "worklog",
|
|
4162
|
+
title: "Worklog",
|
|
4163
|
+
description: "\uB0A0\uC9DC\uBCC4 \uC791\uC5C5 \uAE30\uB85D. `YYYY/MM/YYYY-MM-DD-keyword.md` \uAD6C\uC870.",
|
|
4164
|
+
privacy: "internal",
|
|
4165
|
+
recursive: true,
|
|
4166
|
+
skipPrefixes: [],
|
|
4167
|
+
skipFilenames: []
|
|
4168
|
+
},
|
|
4169
|
+
{
|
|
4170
|
+
dir: "decision-log",
|
|
4171
|
+
title: "Decision Log",
|
|
4172
|
+
description: "\uAC1C\uC778 \uC758\uC0AC\uACB0\uC815 \uAE30\uB85D \u2014 \uC65C \uADF8\uAC78 \uACE8\uB790\uB294\uC9C0\uB97C \uB0A8\uAE41\uB2C8\uB2E4.",
|
|
4173
|
+
privacy: "personal",
|
|
4174
|
+
recursive: false,
|
|
4175
|
+
skipPrefixes: ["_TEMPLATE"],
|
|
4176
|
+
skipFilenames: []
|
|
4177
|
+
},
|
|
4178
|
+
{
|
|
4179
|
+
dir: "runbooks",
|
|
4180
|
+
title: "Runbooks",
|
|
4181
|
+
description: "\uC7A5\uC560 \uB300\uC751\xB7\uC815\uAE30 \uC815\uBE44 \uC808\uCC28. `last_tested`\uB85C \uAC31\uC2E0 \uAE30\uD55C \uCD94\uC801.",
|
|
4182
|
+
privacy: "internal",
|
|
4183
|
+
recursive: false,
|
|
4184
|
+
skipPrefixes: [],
|
|
4185
|
+
skipFilenames: []
|
|
4186
|
+
},
|
|
4187
|
+
{
|
|
4188
|
+
dir: "hubs",
|
|
4189
|
+
title: "Hubs",
|
|
4190
|
+
description: "\uC8FC\uC81C\uBCC4 \uC9C4\uC785\uC810 \u2014 \uAD00\uB828 \uC790\uB8CC\uB97C \uD55C \uACF3\uC5D0\uC11C \uBB36\uB294 \uC778\uB371\uC2A4 \uD398\uC774\uC9C0 \uBAA8\uC74C.",
|
|
4191
|
+
privacy: "internal",
|
|
4192
|
+
recursive: false,
|
|
4193
|
+
skipPrefixes: [],
|
|
4194
|
+
skipFilenames: []
|
|
4195
|
+
},
|
|
4196
|
+
{
|
|
4197
|
+
dir: "projects",
|
|
4198
|
+
title: "Projects",
|
|
4199
|
+
description: "\uC9C4\uD589 \uC911\xB7\uC644\uB8CC \uD504\uB85C\uC81D\uD2B8 \uBAA8\uC74C (Work\xB7Home). \uBCF8 _INDEX\uB294 \uD3C9\uD0C4 \uBAA9\uB85D\uC774\uBA70, \uD558\uC704 \uD504\uB85C\uC81D\uD2B8 \uD3F4\uB354\uBCC4 _INDEX\uAC00 \uB354 \uC720\uC6A9\uD560 \uC218 \uC788\uC74C (\uB2E4\uC74C \uC0AC\uC774\uD074 \uAC80\uD1A0).",
|
|
4200
|
+
privacy: "internal",
|
|
4201
|
+
recursive: true,
|
|
4202
|
+
skipPrefixes: [],
|
|
4203
|
+
skipFilenames: []
|
|
4204
|
+
},
|
|
4205
|
+
{
|
|
4206
|
+
dir: "reference",
|
|
4207
|
+
title: "Reference",
|
|
4208
|
+
description: "AI \uADDC\uCE59\xB7\uAE30\uC220 \uCE58\uD2B8\uC2DC\uD2B8\xB7\uC7A5\uBE44\xB7\uC2A4\uD0AC \uB4F1 \uB808\uD37C\uB7F0\uC2A4 \uC790\uB8CC (AI-Rules\xB7Games\xB7Gear\xB7Infra\xB7Org\xB7Skills\xB7Tech).",
|
|
4209
|
+
privacy: "internal",
|
|
4210
|
+
recursive: true,
|
|
4211
|
+
skipPrefixes: [],
|
|
4212
|
+
skipFilenames: []
|
|
4213
|
+
},
|
|
4214
|
+
{
|
|
4215
|
+
dir: "reports",
|
|
4216
|
+
title: "Reports",
|
|
4217
|
+
description: "\uC815\uAE30 \uAC74\uAC15\uAC80\uC9C4 \uB9AC\uD3EC\uD2B8 (Service-Health\xB7Infra-Health). \uC2DC\uC810 \uC2A4\uB0C5\uC0F7 \uB204\uC801.",
|
|
4218
|
+
privacy: "internal",
|
|
4219
|
+
recursive: true,
|
|
4220
|
+
skipPrefixes: [],
|
|
4221
|
+
skipFilenames: []
|
|
4222
|
+
},
|
|
4223
|
+
{
|
|
4224
|
+
dir: "inbox",
|
|
4225
|
+
title: "Inbox",
|
|
4226
|
+
description: "\uBBF8\uBD84\uB958 \uC784\uC2DC \uBA54\uBAA8\xB7\uC544\uC774\uB514\uC5B4\xB7\uBC31\uB85C\uADF8. \uC815\uC2DD \uBD84\uB958 \uC2DC \uD574\uB2F9 \uCE74\uD14C\uACE0\uB9AC\uB85C \uC774\uB3D9.",
|
|
4227
|
+
privacy: "internal",
|
|
4228
|
+
recursive: true,
|
|
4229
|
+
skipPrefixes: [],
|
|
4230
|
+
skipFilenames: []
|
|
4231
|
+
},
|
|
4232
|
+
{
|
|
4233
|
+
dir: "_templates",
|
|
4234
|
+
title: "Templates",
|
|
4235
|
+
description: "\uBCF4\uACE0\uC6A9\xB7\uACF5\uC720\uC6A9 \uC790\uB8CC \uD15C\uD50C\uB9BF \uBAA8\uC74C (Sharing \uB4F1).",
|
|
4236
|
+
privacy: "internal",
|
|
4237
|
+
recursive: true,
|
|
4238
|
+
skipPrefixes: [],
|
|
4239
|
+
skipFilenames: []
|
|
4240
|
+
}
|
|
4241
|
+
];
|
|
4242
|
+
var reindexCommand = {
|
|
4243
|
+
name: "reindex",
|
|
4244
|
+
description: "Regenerate _INDEX.md for any configured target directory (or all targets when called with no argument). Idempotent \u2014 unchanged indexes are not rewritten.",
|
|
4245
|
+
args: [
|
|
4246
|
+
{
|
|
4247
|
+
name: "dir",
|
|
4248
|
+
description: "Optional directory name. Omit to reindex all targets."
|
|
4249
|
+
}
|
|
4250
|
+
],
|
|
4251
|
+
handler: async (input) => {
|
|
4252
|
+
const requested = input.args.dir;
|
|
4253
|
+
const targets = requested ? TARGETS.filter((t) => t.dir === requested) : TARGETS;
|
|
4254
|
+
if (requested && targets.length === 0) {
|
|
4255
|
+
throw new Error(`Unknown reindex target: "${requested}". Known: ${TARGETS.map((t) => t.dir).join(", ")}`);
|
|
4256
|
+
}
|
|
4257
|
+
const results = [];
|
|
4258
|
+
for (const t of targets) {
|
|
4259
|
+
const dir = join20(input.context.dataDir, t.dir);
|
|
4260
|
+
if (!existsSync6(dir)) {
|
|
4261
|
+
results.push({ dir: t.dir, status: "missing", entries: 0, bytes: 0 });
|
|
4262
|
+
continue;
|
|
4263
|
+
}
|
|
4264
|
+
const entries = await scanDirectory(dir, {
|
|
4265
|
+
recursive: t.recursive,
|
|
4266
|
+
skipPrefixes: t.skipPrefixes,
|
|
4267
|
+
skipFilenames: t.skipFilenames
|
|
4268
|
+
});
|
|
4269
|
+
const body = renderIndex({
|
|
4270
|
+
title: t.title,
|
|
4271
|
+
description: t.description,
|
|
4272
|
+
entries,
|
|
4273
|
+
privacy: t.privacy
|
|
4274
|
+
});
|
|
4275
|
+
const target = join20(dir, "_INDEX.md");
|
|
4276
|
+
let existing;
|
|
4277
|
+
try {
|
|
4278
|
+
existing = await readFile16(target, "utf8");
|
|
4279
|
+
} catch {
|
|
4280
|
+
existing = void 0;
|
|
4281
|
+
}
|
|
4282
|
+
if (existing === body) {
|
|
4283
|
+
results.push({
|
|
4284
|
+
dir: t.dir,
|
|
4285
|
+
status: "unchanged",
|
|
4286
|
+
entries: entries.length,
|
|
4287
|
+
bytes: body.length
|
|
4288
|
+
});
|
|
4289
|
+
continue;
|
|
4290
|
+
}
|
|
4291
|
+
await writeFile9(target, body, "utf8");
|
|
4292
|
+
results.push({
|
|
4293
|
+
dir: t.dir,
|
|
4294
|
+
status: "written",
|
|
4295
|
+
entries: entries.length,
|
|
4296
|
+
bytes: body.length
|
|
4297
|
+
});
|
|
4298
|
+
}
|
|
4299
|
+
return results;
|
|
4300
|
+
}
|
|
4301
|
+
};
|
|
4302
|
+
|
|
4303
|
+
// ../plugins/session-rituals/dist/commands/session-start.js
|
|
4304
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4305
|
+
import { readdir as readdir14 } from "fs/promises";
|
|
4306
|
+
import { join as join21 } from "path";
|
|
4307
|
+
var COUNTED_DIRS = ["_memory", "worklog", "decision-log"];
|
|
4308
|
+
var sessionStartCommand = {
|
|
4309
|
+
name: "session-start",
|
|
4310
|
+
description: "Emit a start-of-session report (time + data directory counts).",
|
|
4311
|
+
handler: async (input) => {
|
|
4312
|
+
const { dataDir, repoRoot } = input.context;
|
|
4313
|
+
const counts = {};
|
|
4314
|
+
const missing = [];
|
|
4315
|
+
for (const name of COUNTED_DIRS) {
|
|
4316
|
+
const dir = join21(dataDir, name);
|
|
4317
|
+
if (!existsSync7(dir)) {
|
|
4318
|
+
missing.push(name);
|
|
4319
|
+
counts[name] = 0;
|
|
4320
|
+
continue;
|
|
4321
|
+
}
|
|
4322
|
+
counts[name] = await countMarkdown(dir, name === "worklog");
|
|
4323
|
+
}
|
|
4324
|
+
return {
|
|
4325
|
+
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4326
|
+
repoRoot,
|
|
4327
|
+
dataDir,
|
|
4328
|
+
counts,
|
|
4329
|
+
missing
|
|
4330
|
+
};
|
|
4331
|
+
}
|
|
4332
|
+
};
|
|
4333
|
+
async function countMarkdown(dir, recursive) {
|
|
4334
|
+
let total = 0;
|
|
4335
|
+
const entries = await readdir14(dir, { withFileTypes: true });
|
|
4336
|
+
for (const e of entries) {
|
|
4337
|
+
if (e.isFile()) {
|
|
4338
|
+
if (!e.name.endsWith(".md"))
|
|
4339
|
+
continue;
|
|
4340
|
+
if (e.name === "README.md" || e.name === "_INDEX.md" || e.name === "MEMORY.md") {
|
|
4341
|
+
continue;
|
|
4342
|
+
}
|
|
4343
|
+
if (e.name.startsWith("_TEMPLATE"))
|
|
4344
|
+
continue;
|
|
4345
|
+
total++;
|
|
4346
|
+
} else if (e.isDirectory() && recursive) {
|
|
4347
|
+
if (e.name.startsWith(".") || e.name.startsWith("_"))
|
|
4348
|
+
continue;
|
|
4349
|
+
total += await countMarkdown(join21(dir, e.name), recursive);
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
return total;
|
|
4353
|
+
}
|
|
4354
|
+
|
|
4355
|
+
// ../plugins/session-rituals/dist/commands/log.js
|
|
4356
|
+
var logCommand = {
|
|
4357
|
+
name: "log",
|
|
4358
|
+
description: "Append a `## <section-title>` section to today's worklog entry. Throws if today's worklog does not exist (run `/vortex init` once to bootstrap).",
|
|
4359
|
+
args: [
|
|
4360
|
+
{
|
|
4361
|
+
name: "section",
|
|
4362
|
+
description: "Title for the new `## ` section (rest of the input).",
|
|
4363
|
+
required: true
|
|
4364
|
+
}
|
|
4365
|
+
],
|
|
4366
|
+
handler: async (input) => {
|
|
4367
|
+
const sectionTitle = input.rest.trim();
|
|
4368
|
+
if (!sectionTitle) {
|
|
4369
|
+
throw new Error("`/log` requires a section title.");
|
|
4370
|
+
}
|
|
4371
|
+
const date = todayIso2();
|
|
4372
|
+
const store = new WorklogStore(`${input.context.dataDir}/worklog`);
|
|
4373
|
+
const todayEntry = await store.get(date);
|
|
4374
|
+
if (!todayEntry) {
|
|
4375
|
+
throw new Error(`No worklog entry exists for ${date}. Create one first; this command only appends sections.`);
|
|
4376
|
+
}
|
|
4377
|
+
await appendSection(todayEntry, sectionTitle, "");
|
|
4378
|
+
return {
|
|
4379
|
+
path: todayEntry.path,
|
|
4380
|
+
date: todayEntry.date,
|
|
4381
|
+
keyword: todayEntry.keyword,
|
|
4382
|
+
sectionTitle
|
|
4383
|
+
};
|
|
4384
|
+
}
|
|
4385
|
+
};
|
|
4386
|
+
function todayIso2() {
|
|
4387
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
4388
|
+
const y2 = d2.getFullYear();
|
|
4389
|
+
const m2 = String(d2.getMonth() + 1).padStart(2, "0");
|
|
4390
|
+
const day = String(d2.getDate()).padStart(2, "0");
|
|
4391
|
+
return `${y2}-${m2}-${day}`;
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4394
|
+
// ../plugins/session-rituals/dist/commands/vortex.js
|
|
4395
|
+
import { spawn } from "child_process";
|
|
4396
|
+
import { existsSync as existsSync8 } from "fs";
|
|
4397
|
+
import { copyFile, mkdir as mkdir5, readdir as readdir15, readFile as readFile17, stat as stat8, writeFile as writeFile10 } from "fs/promises";
|
|
4398
|
+
import { basename as basename7, dirname as dirname4, join as join22 } from "path";
|
|
4399
|
+
import { fileURLToPath } from "url";
|
|
4400
|
+
|
|
4401
|
+
// ../plugins/session-rituals/dist/ensure-hooks.js
|
|
4402
|
+
var SESSION_START_COMMAND = "node plugins/session-rituals/scripts/session-start-hook.mjs";
|
|
4403
|
+
var SESSION_END_COMMAND = "node plugins/session-rituals/scripts/session-end-hook.mjs";
|
|
4404
|
+
function parseSettings(text) {
|
|
4405
|
+
const trimmed = (text ?? "").trim();
|
|
4406
|
+
if (trimmed.length === 0)
|
|
4407
|
+
return {};
|
|
4408
|
+
let parsed;
|
|
4409
|
+
try {
|
|
4410
|
+
parsed = JSON.parse(trimmed);
|
|
4411
|
+
} catch (e) {
|
|
4412
|
+
throw new Error(`.claude/settings.json is not valid JSON \u2014 refusing to overwrite. Fix or remove it first. (${e.message})`);
|
|
4413
|
+
}
|
|
4414
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4415
|
+
throw new Error(".claude/settings.json is not a JSON object \u2014 refusing to overwrite.");
|
|
4416
|
+
}
|
|
4417
|
+
return parsed;
|
|
4418
|
+
}
|
|
4419
|
+
function hasCommand(groups, command) {
|
|
4420
|
+
if (!groups)
|
|
4421
|
+
return false;
|
|
4422
|
+
return groups.some((g) => g.hooks?.some((h) => h.command === command));
|
|
4423
|
+
}
|
|
4424
|
+
function ensureVortexHooks(existing) {
|
|
4425
|
+
const base = existing && typeof existing === "object" ? existing : {};
|
|
4426
|
+
const hooks = { ...base.hooks ?? {} };
|
|
4427
|
+
const added = [];
|
|
4428
|
+
const wire = (event, command) => {
|
|
4429
|
+
const groups = hooks[event] ? [...hooks[event]] : [];
|
|
4430
|
+
if (hasCommand(groups, command))
|
|
4431
|
+
return;
|
|
4432
|
+
groups.push({ hooks: [{ type: "command", command }] });
|
|
4433
|
+
hooks[event] = groups;
|
|
4434
|
+
added.push(event);
|
|
4435
|
+
};
|
|
4436
|
+
wire("SessionStart", SESSION_START_COMMAND);
|
|
4437
|
+
wire("SessionEnd", SESSION_END_COMMAND);
|
|
4438
|
+
const settings = { ...base, hooks };
|
|
4439
|
+
return { settings, added, alreadyWired: added.length === 0 };
|
|
4440
|
+
}
|
|
4441
|
+
function serializeSettings(settings) {
|
|
4442
|
+
return JSON.stringify(settings, null, 2) + "\n";
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
// ../plugins/session-rituals/dist/commands/vortex.js
|
|
4446
|
+
var PLANNED_SUBS = [];
|
|
4447
|
+
var vortexCommand = {
|
|
4448
|
+
name: "vortex",
|
|
4449
|
+
description: "VortEX root command. Subcommands: init | status | import | doctor | sync | help.",
|
|
4450
|
+
args: [
|
|
4451
|
+
{
|
|
4452
|
+
name: "sub",
|
|
4453
|
+
description: "Subcommand (init|status|import|doctor|sync|help).",
|
|
4454
|
+
required: false
|
|
4455
|
+
}
|
|
4456
|
+
],
|
|
4457
|
+
handler: async (input) => {
|
|
4458
|
+
const tokens = tokenize(input.rest);
|
|
4459
|
+
const sub = tokens[0] ?? "help";
|
|
4460
|
+
const restAfterSub = tokens.slice(1);
|
|
4461
|
+
if (sub === "init")
|
|
4462
|
+
return runInit(input, restAfterSub);
|
|
4463
|
+
if (sub === "status")
|
|
4464
|
+
return runStatus(input);
|
|
4465
|
+
if (sub === "import")
|
|
4466
|
+
return runImport(input, restAfterSub);
|
|
4467
|
+
if (sub === "doctor")
|
|
4468
|
+
return runDoctor(input);
|
|
4469
|
+
if (sub === "sync")
|
|
4470
|
+
return runSync(input, restAfterSub);
|
|
4471
|
+
if (sub === "help" || sub === "")
|
|
4472
|
+
return runHelp();
|
|
4473
|
+
if (PLANNED_SUBS.includes(sub)) {
|
|
4474
|
+
return {
|
|
4475
|
+
subcommand: sub,
|
|
4476
|
+
status: "not-implemented",
|
|
4477
|
+
message: `\`/vortex ${sub}\` is reserved but not yet implemented. Planned in a future phase. Run \`/vortex help\` for available subcommands.`
|
|
4478
|
+
};
|
|
4479
|
+
}
|
|
4480
|
+
return {
|
|
4481
|
+
subcommand: "unknown",
|
|
4482
|
+
status: "not-implemented",
|
|
4483
|
+
message: `Unknown subcommand "${sub}". Run \`/vortex help\` for the list.`
|
|
4484
|
+
};
|
|
4485
|
+
}
|
|
4486
|
+
};
|
|
4487
|
+
function runHelp() {
|
|
4488
|
+
return {
|
|
4489
|
+
subcommand: "help",
|
|
4490
|
+
status: "ok",
|
|
4491
|
+
subcommands: [
|
|
4492
|
+
{
|
|
4493
|
+
name: "init",
|
|
4494
|
+
description: "First-time setup wizard. Creates user profile memory and first worklog (hubs grow organically \u2014 not seeded here).",
|
|
4495
|
+
state: "active"
|
|
4496
|
+
},
|
|
4497
|
+
{
|
|
4498
|
+
name: "status",
|
|
4499
|
+
description: "Show instance state (memory count, latest worklog, missing skeletons).",
|
|
4500
|
+
state: "active"
|
|
4501
|
+
},
|
|
4502
|
+
{
|
|
4503
|
+
name: "import",
|
|
4504
|
+
description: "Bring an existing folder into data/ \u2014 preserves your folder structure, auto-classifies worklog/decision-log/runbooks/hubs/_memory files, injects missing frontmatter.",
|
|
4505
|
+
state: "active"
|
|
4506
|
+
},
|
|
4507
|
+
{
|
|
4508
|
+
name: "doctor",
|
|
4509
|
+
description: "Diagnose instance health (system dirs, profile, indexes, wiki links, frontmatter, runbook freshness, node version, git remote).",
|
|
4510
|
+
state: "active"
|
|
4511
|
+
},
|
|
4512
|
+
{
|
|
4513
|
+
name: "sync",
|
|
4514
|
+
description: "Framework-developer workflow: git pull \u2192 npm install \u2192 npm run build \u2192 npm run verify. Stops on first failure. End users on npm registry do not need this.",
|
|
4515
|
+
state: "active"
|
|
4516
|
+
},
|
|
4517
|
+
{ name: "help", description: "Show this list.", state: "active" }
|
|
4518
|
+
],
|
|
4519
|
+
siblingCommands: [
|
|
4520
|
+
{
|
|
4521
|
+
name: "session-start",
|
|
4522
|
+
description: "Emit a start-of-session report (time + data directory counts + missing dirs)."
|
|
4523
|
+
},
|
|
4524
|
+
{
|
|
4525
|
+
name: "log",
|
|
4526
|
+
description: "Append a `## <section-title>` section to today's worklog entry. Throws if today's worklog does not exist (run `/vortex init` once to bootstrap)."
|
|
4527
|
+
},
|
|
4528
|
+
{
|
|
4529
|
+
name: "decision",
|
|
4530
|
+
description: "Create a new Decision Log entry from the canonical template at `data/decision-log/<today>-<slug>.md`. Refuses to overwrite an existing file."
|
|
4531
|
+
},
|
|
4532
|
+
{
|
|
4533
|
+
name: "reindex",
|
|
4534
|
+
description: "Regenerate _INDEX.md for any configured target directory (or all targets when called with no argument). Idempotent \u2014 unchanged indexes are not rewritten."
|
|
4535
|
+
}
|
|
4536
|
+
]
|
|
4537
|
+
};
|
|
4538
|
+
}
|
|
4539
|
+
async function installCommandTemplates(repoRoot) {
|
|
4540
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
4541
|
+
const templatesDir = join22(here, "..", "..", "templates", "commands");
|
|
4542
|
+
if (!existsSync8(templatesDir))
|
|
4543
|
+
return [];
|
|
4544
|
+
const destDir = join22(repoRoot, ".claude", "commands");
|
|
4545
|
+
await mkdir5(destDir, { recursive: true });
|
|
4546
|
+
const written = [];
|
|
4547
|
+
for (const name of await readdir15(templatesDir)) {
|
|
4548
|
+
if (!name.endsWith(".md"))
|
|
4549
|
+
continue;
|
|
4550
|
+
const dest = join22(destDir, name);
|
|
4551
|
+
if (existsSync8(dest))
|
|
4552
|
+
continue;
|
|
4553
|
+
await copyFile(join22(templatesDir, name), dest);
|
|
4554
|
+
written.push(dest);
|
|
4555
|
+
}
|
|
4556
|
+
return written;
|
|
4557
|
+
}
|
|
4558
|
+
async function runInit(input, tokens) {
|
|
4559
|
+
const args = parseInitArgs(tokens);
|
|
4560
|
+
const { dataDir } = input.context;
|
|
4561
|
+
const requiredDirs = ["_memory", "worklog", "decision-log", "hubs", "inbox", "runbooks"];
|
|
4562
|
+
for (const d2 of requiredDirs) {
|
|
4563
|
+
const p = join22(dataDir, d2);
|
|
4564
|
+
if (!existsSync8(p))
|
|
4565
|
+
await mkdir5(p, { recursive: true });
|
|
4566
|
+
}
|
|
4567
|
+
const profilePath = join22(dataDir, "_memory", "user_profile.md");
|
|
4568
|
+
if (existsSync8(profilePath) && !args.force) {
|
|
4569
|
+
return {
|
|
4570
|
+
subcommand: "init",
|
|
4571
|
+
status: "already-initialized",
|
|
4572
|
+
created: [],
|
|
4573
|
+
nextActions: [
|
|
4574
|
+
`VortEX instance is already initialized (${profilePath} exists).`,
|
|
4575
|
+
"To re-run, pass `--force` (existing user_profile / first worklog / first hub will be overwritten).",
|
|
4576
|
+
"To check current state, try `/session-start`."
|
|
4577
|
+
]
|
|
4578
|
+
};
|
|
4579
|
+
}
|
|
4580
|
+
const missing = [];
|
|
4581
|
+
if (!args.name) {
|
|
4582
|
+
missing.push({
|
|
4583
|
+
name: "name",
|
|
4584
|
+
prompt: 'What name or handle should VortEX use for you? (e.g. "Alex" or "team-lead")'
|
|
4585
|
+
});
|
|
4586
|
+
}
|
|
4587
|
+
if (!args.role) {
|
|
4588
|
+
missing.push({
|
|
4589
|
+
name: "role",
|
|
4590
|
+
prompt: 'What is your main role in one word? (e.g. "engineer", "researcher", "writer")'
|
|
4591
|
+
});
|
|
4592
|
+
}
|
|
4593
|
+
if (!args.task) {
|
|
4594
|
+
missing.push({
|
|
4595
|
+
name: "task",
|
|
4596
|
+
prompt: "What is one thing you're working on right now? (one sentence \u2014 this becomes your first worklog seed)"
|
|
4597
|
+
});
|
|
4598
|
+
}
|
|
4599
|
+
if (missing.length > 0) {
|
|
4600
|
+
return {
|
|
4601
|
+
subcommand: "init",
|
|
4602
|
+
status: "needs-input",
|
|
4603
|
+
created: [],
|
|
4604
|
+
missingInputs: missing,
|
|
4605
|
+
nextActions: [
|
|
4606
|
+
"Ask the user the prompts in `missingInputs`, then re-run with the answers:",
|
|
4607
|
+
' /vortex init --name "<name>" --role "<role>" --task "<task>"',
|
|
4608
|
+
"Optional: append `--force` to overwrite an already-initialized instance."
|
|
4609
|
+
]
|
|
4610
|
+
};
|
|
4611
|
+
}
|
|
4612
|
+
const today2 = todayIso3();
|
|
4613
|
+
const created = [];
|
|
4614
|
+
await writeFile10(profilePath, renderUserProfile(args.name, args.role, args.task, today2), "utf8");
|
|
4615
|
+
created.push(profilePath);
|
|
4616
|
+
const [year, month] = today2.split("-");
|
|
4617
|
+
const worklogDir = join22(dataDir, "worklog", year, month);
|
|
4618
|
+
await mkdir5(worklogDir, { recursive: true });
|
|
4619
|
+
const worklogPath = join22(worklogDir, `${today2}-vortex-init.md`);
|
|
4620
|
+
await writeFile10(worklogPath, renderFirstWorklog(args.name, args.role, args.task, today2), "utf8");
|
|
4621
|
+
created.push(worklogPath);
|
|
4622
|
+
const hookNotes = [];
|
|
4623
|
+
try {
|
|
4624
|
+
const settingsPath = join22(input.context.repoRoot, ".claude", "settings.json");
|
|
4625
|
+
const existingText = existsSync8(settingsPath) ? await readFile17(settingsPath, "utf8") : null;
|
|
4626
|
+
const { settings, added, alreadyWired } = ensureVortexHooks(parseSettings(existingText));
|
|
4627
|
+
if (!alreadyWired) {
|
|
4628
|
+
await mkdir5(join22(input.context.repoRoot, ".claude"), { recursive: true });
|
|
4629
|
+
await writeFile10(settingsPath, serializeSettings(settings), "utf8");
|
|
4630
|
+
created.push(settingsPath);
|
|
4631
|
+
hookNotes.push(`Wired ${added.join(" + ")} hook(s) into .claude/settings.json \u2014 the VortEX boot report runs automatically at session start.`);
|
|
4632
|
+
} else {
|
|
4633
|
+
hookNotes.push("Session hooks already wired in .claude/settings.json.");
|
|
4634
|
+
}
|
|
4635
|
+
} catch (e) {
|
|
4636
|
+
hookNotes.push(`\u26A0\uFE0F Could not wire session hooks automatically: ${e.message} Copy .claude/settings.example.json to .claude/settings.json by hand to enable the boot report.`);
|
|
4637
|
+
}
|
|
4638
|
+
try {
|
|
4639
|
+
const cmds = await installCommandTemplates(input.context.repoRoot);
|
|
4640
|
+
created.push(...cmds);
|
|
4641
|
+
if (cmds.length > 0) {
|
|
4642
|
+
hookNotes.push(`Installed ${cmds.length} slash command(s) into .claude/commands/ (${cmds.map((c) => "/" + basename7(c, ".md")).join(", ")}).`);
|
|
4643
|
+
}
|
|
4644
|
+
} catch (e) {
|
|
4645
|
+
hookNotes.push(`\u26A0\uFE0F Could not install slash commands: ${e.message}`);
|
|
4646
|
+
}
|
|
4647
|
+
const externalFolders = await detectExternalFolders(input.context.repoRoot);
|
|
4648
|
+
const baseNext = [
|
|
4649
|
+
`Done. Created ${created.length} files.`,
|
|
4650
|
+
...hookNotes,
|
|
4651
|
+
"Next 3 things you can try right now:",
|
|
4652
|
+
" /log <one-line update> \u2014 append a section to today's worklog",
|
|
4653
|
+
" /decision <slug> <title> \u2014 record a decision",
|
|
4654
|
+
" /session-start \u2014 daily start-of-session report",
|
|
4655
|
+
`Open ${worklogPath} to see your first worklog \u2014 it already names "${args.task}".`,
|
|
4656
|
+
"Hubs grow organically: once 3+ categories accumulate on the same topic, create `hubs/_HUB-<topic>.md` to cross-link them."
|
|
4657
|
+
];
|
|
4658
|
+
const importPrompt = [];
|
|
4659
|
+
if (externalFolders && externalFolders.length > 0) {
|
|
4660
|
+
importPrompt.push("");
|
|
4661
|
+
importPrompt.push("\u{1F4C1} Detected existing folders that look like notes/vaults you may want to import:");
|
|
4662
|
+
for (const f of externalFolders) {
|
|
4663
|
+
importPrompt.push(` - ${f.path} (${f.mdCount} .md files)`);
|
|
4664
|
+
}
|
|
4665
|
+
importPrompt.push(" Import any of them now with:");
|
|
4666
|
+
for (const f of externalFolders) {
|
|
4667
|
+
importPrompt.push(` /vortex import --from "${f.path}"`);
|
|
4668
|
+
}
|
|
4669
|
+
importPrompt.push(" (Append --dry-run first to preview without copying. You can also do this later \u2014 these folders are not modified.)");
|
|
4670
|
+
}
|
|
4671
|
+
return {
|
|
4672
|
+
subcommand: "init",
|
|
4673
|
+
status: "completed",
|
|
4674
|
+
created,
|
|
4675
|
+
externalFolders,
|
|
4676
|
+
nextActions: [...baseNext, ...importPrompt]
|
|
4677
|
+
};
|
|
4678
|
+
}
|
|
4679
|
+
function parseInitArgs(tokens) {
|
|
4680
|
+
const args = {};
|
|
4681
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
4682
|
+
const t = tokens[i];
|
|
4683
|
+
if (t === "--force") {
|
|
4684
|
+
args.force = true;
|
|
4685
|
+
continue;
|
|
4686
|
+
}
|
|
4687
|
+
if (t === "--name" && i + 1 < tokens.length) {
|
|
4688
|
+
args.name = tokens[++i];
|
|
4689
|
+
continue;
|
|
4690
|
+
}
|
|
4691
|
+
if (t === "--role" && i + 1 < tokens.length) {
|
|
4692
|
+
args.role = tokens[++i];
|
|
4693
|
+
continue;
|
|
4694
|
+
}
|
|
4695
|
+
if (t === "--task" && i + 1 < tokens.length) {
|
|
4696
|
+
args.task = tokens[++i];
|
|
4697
|
+
continue;
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
return args;
|
|
4701
|
+
}
|
|
4702
|
+
function tokenize(s) {
|
|
4703
|
+
const out = [];
|
|
4704
|
+
let i = 0;
|
|
4705
|
+
while (i < s.length) {
|
|
4706
|
+
while (i < s.length && /\s/.test(s[i]))
|
|
4707
|
+
i++;
|
|
4708
|
+
if (i >= s.length)
|
|
4709
|
+
break;
|
|
4710
|
+
const ch = s[i];
|
|
4711
|
+
if (ch === '"' || ch === "'") {
|
|
4712
|
+
const quote = ch;
|
|
4713
|
+
i++;
|
|
4714
|
+
let buf = "";
|
|
4715
|
+
while (i < s.length && s[i] !== quote) {
|
|
4716
|
+
buf += s[i++];
|
|
4717
|
+
}
|
|
4718
|
+
if (i < s.length)
|
|
4719
|
+
i++;
|
|
4720
|
+
out.push(buf);
|
|
4721
|
+
} else {
|
|
4722
|
+
let buf = "";
|
|
4723
|
+
while (i < s.length && !/\s/.test(s[i])) {
|
|
4724
|
+
buf += s[i++];
|
|
4725
|
+
}
|
|
4726
|
+
out.push(buf);
|
|
4727
|
+
}
|
|
4728
|
+
}
|
|
4729
|
+
return out;
|
|
4730
|
+
}
|
|
4731
|
+
function renderUserProfile(name, role, task, date) {
|
|
4732
|
+
return `---
|
|
4733
|
+
name: user-profile
|
|
4734
|
+
description: Operator profile captured by /vortex init.
|
|
4735
|
+
type: user
|
|
4736
|
+
created: ${date}
|
|
4737
|
+
updated: ${date}
|
|
4738
|
+
---
|
|
4739
|
+
|
|
4740
|
+
# User Profile
|
|
4741
|
+
|
|
4742
|
+
- **Name/handle**: ${name}
|
|
4743
|
+
- **Role**: ${role}
|
|
4744
|
+
- **Initial focus**: ${task}
|
|
4745
|
+
|
|
4746
|
+
This memory was created by \`/vortex init\` on ${date}. Edit freely as your role evolves.
|
|
4747
|
+
`;
|
|
4748
|
+
}
|
|
4749
|
+
function renderFirstWorklog(name, role, task, date) {
|
|
4750
|
+
return `---
|
|
4751
|
+
type: worklog
|
|
4752
|
+
status: active
|
|
4753
|
+
created: ${date}
|
|
4754
|
+
updated: ${date}
|
|
4755
|
+
tags: [worklog, onboarding]
|
|
4756
|
+
---
|
|
4757
|
+
|
|
4758
|
+
# ${date} \u2014 VortEX \uC2DC\uC791
|
|
4759
|
+
|
|
4760
|
+
> First worklog, created by \`/vortex init\`. ${name} (${role}). Today's focus: ${task}
|
|
4761
|
+
|
|
4762
|
+
## What I'm working on
|
|
4763
|
+
|
|
4764
|
+
${task}
|
|
4765
|
+
|
|
4766
|
+
## Notes
|
|
4767
|
+
|
|
4768
|
+
(append more with \`/log <section-title>\`)
|
|
4769
|
+
|
|
4770
|
+
## Next
|
|
4771
|
+
|
|
4772
|
+
- [ ] Try \`/decision <slug> <title>\` for your first decision record
|
|
4773
|
+
- [ ] Add a memory by editing \`data/_memory/user_profile.md\` or creating new ones
|
|
4774
|
+
- [ ] Run \`/session-start\` tomorrow to see your accumulated state
|
|
4775
|
+
`;
|
|
4776
|
+
}
|
|
4777
|
+
function todayIso3() {
|
|
4778
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
4779
|
+
const y2 = d2.getFullYear();
|
|
4780
|
+
const m2 = String(d2.getMonth() + 1).padStart(2, "0");
|
|
4781
|
+
const day = String(d2.getDate()).padStart(2, "0");
|
|
4782
|
+
return `${y2}-${m2}-${day}`;
|
|
4783
|
+
}
|
|
4784
|
+
var COUNT_KEY_TO_DIR = {
|
|
4785
|
+
memory: "_memory",
|
|
4786
|
+
worklog: "worklog",
|
|
4787
|
+
decisionLog: "decision-log",
|
|
4788
|
+
runbooks: "runbooks",
|
|
4789
|
+
hubs: "hubs"
|
|
4790
|
+
};
|
|
4791
|
+
async function runStatus(input) {
|
|
4792
|
+
const { dataDir } = input.context;
|
|
4793
|
+
const profilePath = join22(dataDir, "_memory", "user_profile.md");
|
|
4794
|
+
const initialized = existsSync8(profilePath);
|
|
4795
|
+
const counts = {
|
|
4796
|
+
memory: await safeCount(join22(dataDir, "_memory"), false),
|
|
4797
|
+
worklog: await safeCount(join22(dataDir, "worklog"), true),
|
|
4798
|
+
decisionLog: await safeCount(join22(dataDir, "decision-log"), false),
|
|
4799
|
+
runbooks: await safeCount(join22(dataDir, "runbooks"), false),
|
|
4800
|
+
hubs: await safeCount(join22(dataDir, "hubs"), false)
|
|
4801
|
+
};
|
|
4802
|
+
let latestWorklog;
|
|
4803
|
+
try {
|
|
4804
|
+
const store = new WorklogStore(join22(dataDir, "worklog"));
|
|
4805
|
+
const latest = await store.getLatest();
|
|
4806
|
+
if (latest) {
|
|
4807
|
+
latestWorklog = {
|
|
4808
|
+
date: latest.date,
|
|
4809
|
+
keyword: latest.keyword,
|
|
4810
|
+
path: latest.path
|
|
4811
|
+
};
|
|
4812
|
+
}
|
|
4813
|
+
} catch {
|
|
4814
|
+
}
|
|
4815
|
+
let profile;
|
|
4816
|
+
if (initialized) {
|
|
4817
|
+
try {
|
|
4818
|
+
const raw = await readFile17(profilePath, "utf8");
|
|
4819
|
+
const { body } = parseFrontmatter(raw);
|
|
4820
|
+
profile = extractProfile(body);
|
|
4821
|
+
} catch {
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
const missing = [];
|
|
4825
|
+
if (!initialized) {
|
|
4826
|
+
missing.push("_memory/user_profile.md \u2014 run `/vortex init`");
|
|
4827
|
+
}
|
|
4828
|
+
for (const [key, count] of Object.entries(counts)) {
|
|
4829
|
+
if (count === 0) {
|
|
4830
|
+
const dirName = COUNT_KEY_TO_DIR[key];
|
|
4831
|
+
const dirPath = join22(dataDir, dirName);
|
|
4832
|
+
missing.push(existsSync8(dirPath) ? `${dirName}/ is empty` : `${dirName}/ does not exist`);
|
|
2895
4833
|
}
|
|
2896
|
-
return {
|
|
2897
|
-
time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2898
|
-
repoRoot,
|
|
2899
|
-
dataDir,
|
|
2900
|
-
counts,
|
|
2901
|
-
missing
|
|
2902
|
-
};
|
|
2903
4834
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
4835
|
+
const nextActions = [];
|
|
4836
|
+
if (!initialized) {
|
|
4837
|
+
nextActions.push("Run `/vortex init --name <name> --role <role> --task <task>` to set up this instance.");
|
|
4838
|
+
} else {
|
|
4839
|
+
nextActions.push("Run `/session-start` for a fuller session-opening report.", "Run `/log <section-title>` to append a section to today's worklog.");
|
|
4840
|
+
if (counts.decisionLog === 0) {
|
|
4841
|
+
nextActions.push("Try `/decision <slug> <title>` to record your first decision.");
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
return {
|
|
4845
|
+
subcommand: "status",
|
|
4846
|
+
status: initialized ? "ok" : "uninitialized",
|
|
4847
|
+
instance: { dataDir, initialized, profile },
|
|
4848
|
+
counts,
|
|
4849
|
+
latestWorklog,
|
|
4850
|
+
missing,
|
|
4851
|
+
nextActions
|
|
4852
|
+
};
|
|
4853
|
+
}
|
|
4854
|
+
function extractProfile(body) {
|
|
4855
|
+
const nameMatch = body.match(/^- \*\*Name\/handle\*\*:\s*(.+)$/m);
|
|
4856
|
+
const roleMatch = body.match(/^- \*\*Role\*\*:\s*(.+)$/m);
|
|
4857
|
+
if (!nameMatch && !roleMatch)
|
|
4858
|
+
return void 0;
|
|
4859
|
+
const out = {};
|
|
4860
|
+
if (nameMatch)
|
|
4861
|
+
out.name = nameMatch[1].trim();
|
|
4862
|
+
if (roleMatch)
|
|
4863
|
+
out.role = roleMatch[1].trim();
|
|
4864
|
+
return out;
|
|
4865
|
+
}
|
|
4866
|
+
async function safeCount(dir, recursive) {
|
|
4867
|
+
if (!existsSync8(dir))
|
|
4868
|
+
return 0;
|
|
4869
|
+
try {
|
|
4870
|
+
return await countMarkdown2(dir, recursive);
|
|
4871
|
+
} catch {
|
|
4872
|
+
return 0;
|
|
4873
|
+
}
|
|
4874
|
+
}
|
|
4875
|
+
async function countMarkdown2(dir, recursive) {
|
|
2906
4876
|
let total = 0;
|
|
2907
|
-
const entries = await
|
|
4877
|
+
const entries = await readdir15(dir, { withFileTypes: true });
|
|
2908
4878
|
for (const e of entries) {
|
|
2909
4879
|
if (e.isFile()) {
|
|
2910
4880
|
if (!e.name.endsWith(".md"))
|
|
@@ -2918,350 +4888,1220 @@ async function countMarkdown(dir, recursive) {
|
|
|
2918
4888
|
} else if (e.isDirectory() && recursive) {
|
|
2919
4889
|
if (e.name.startsWith(".") || e.name.startsWith("_"))
|
|
2920
4890
|
continue;
|
|
2921
|
-
total += await
|
|
4891
|
+
total += await countMarkdown2(join22(dir, e.name), recursive);
|
|
2922
4892
|
}
|
|
2923
4893
|
}
|
|
2924
4894
|
return total;
|
|
2925
4895
|
}
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
4896
|
+
var IMPORT_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
4897
|
+
".git",
|
|
4898
|
+
"node_modules",
|
|
4899
|
+
".vscode",
|
|
4900
|
+
".idea",
|
|
4901
|
+
"dist",
|
|
4902
|
+
"build",
|
|
4903
|
+
".obsidian",
|
|
4904
|
+
".trash"
|
|
4905
|
+
]);
|
|
4906
|
+
var IMPORT_SKIP_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", ".gitkeep"]);
|
|
4907
|
+
var WORKLOG_FOLDER_NAMES = /* @__PURE__ */ new Set([
|
|
4908
|
+
"worklog",
|
|
4909
|
+
"til",
|
|
4910
|
+
"daily",
|
|
4911
|
+
"journal",
|
|
4912
|
+
"diary",
|
|
4913
|
+
"daily-notes",
|
|
4914
|
+
"logs",
|
|
4915
|
+
"log",
|
|
4916
|
+
"\uC77C\uC9C0",
|
|
4917
|
+
"\uB0A0\uC9DC\uBCC4"
|
|
4918
|
+
]);
|
|
4919
|
+
var DECISION_FOLDER_NAMES = /* @__PURE__ */ new Set([
|
|
4920
|
+
"decision-log",
|
|
4921
|
+
"decisions",
|
|
4922
|
+
"decision"
|
|
4923
|
+
]);
|
|
4924
|
+
var RUNBOOK_FOLDER_NAMES = /* @__PURE__ */ new Set(["runbooks", "runbook", "sop"]);
|
|
4925
|
+
var HUB_FOLDER_NAMES = /* @__PURE__ */ new Set(["hubs", "hub", "_hub"]);
|
|
4926
|
+
var MEMORY_FOLDER_NAMES = /* @__PURE__ */ new Set(["_memory", "memory", "memories"]);
|
|
4927
|
+
var LEGACY_WORKLOG_TYPES = /* @__PURE__ */ new Set([
|
|
4928
|
+
"til",
|
|
4929
|
+
"daily",
|
|
4930
|
+
"journal",
|
|
4931
|
+
"diary",
|
|
4932
|
+
"log"
|
|
4933
|
+
]);
|
|
4934
|
+
var FILENAME_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}-/;
|
|
4935
|
+
function parseImportArgs(tokens) {
|
|
4936
|
+
const args = {};
|
|
4937
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
4938
|
+
const t = tokens[i];
|
|
4939
|
+
if (t === "--dry-run") {
|
|
4940
|
+
args.dryRun = true;
|
|
4941
|
+
continue;
|
|
2942
4942
|
}
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
if (!todayEntry) {
|
|
2947
|
-
throw new Error(`No TIL entry exists for ${date}. Create one first; this command only appends sections.`);
|
|
4943
|
+
if (t === "--from" && i + 1 < tokens.length) {
|
|
4944
|
+
args.from = tokens[++i];
|
|
4945
|
+
continue;
|
|
2948
4946
|
}
|
|
2949
|
-
|
|
4947
|
+
}
|
|
4948
|
+
return args;
|
|
4949
|
+
}
|
|
4950
|
+
async function runImport(input, tokens) {
|
|
4951
|
+
const args = parseImportArgs(tokens);
|
|
4952
|
+
const { dataDir } = input.context;
|
|
4953
|
+
const emptyClassified = {
|
|
4954
|
+
worklog: 0,
|
|
4955
|
+
decisionLog: 0,
|
|
4956
|
+
runbooks: 0,
|
|
4957
|
+
hubs: 0,
|
|
4958
|
+
memory: 0,
|
|
4959
|
+
preserved: 0
|
|
4960
|
+
};
|
|
4961
|
+
if (!args.from) {
|
|
2950
4962
|
return {
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
4963
|
+
subcommand: "import",
|
|
4964
|
+
status: "needs-input",
|
|
4965
|
+
totalFiles: 0,
|
|
4966
|
+
copied: 0,
|
|
4967
|
+
classified: emptyClassified,
|
|
4968
|
+
frontmatterInjected: 0,
|
|
4969
|
+
frontmatterPreserved: 0,
|
|
4970
|
+
systemDirsCreated: [],
|
|
4971
|
+
skipped: 0,
|
|
4972
|
+
missingInputs: [
|
|
4973
|
+
{
|
|
4974
|
+
name: "from",
|
|
4975
|
+
prompt: "Where is the folder you want to import? (absolute path, e.g. C:/Users/me/notes)"
|
|
4976
|
+
}
|
|
4977
|
+
],
|
|
4978
|
+
nextActions: [
|
|
4979
|
+
"Re-run with the path:",
|
|
4980
|
+
' /vortex import --from "<absolute path>"',
|
|
4981
|
+
"Optional: append --dry-run to preview without copying."
|
|
4982
|
+
]
|
|
2955
4983
|
};
|
|
2956
4984
|
}
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
4985
|
+
if (!existsSync8(args.from)) {
|
|
4986
|
+
return {
|
|
4987
|
+
subcommand: "import",
|
|
4988
|
+
status: "source-missing",
|
|
4989
|
+
source: args.from,
|
|
4990
|
+
totalFiles: 0,
|
|
4991
|
+
copied: 0,
|
|
4992
|
+
classified: emptyClassified,
|
|
4993
|
+
frontmatterInjected: 0,
|
|
4994
|
+
frontmatterPreserved: 0,
|
|
4995
|
+
systemDirsCreated: [],
|
|
4996
|
+
skipped: 0,
|
|
4997
|
+
nextActions: [
|
|
4998
|
+
`Source folder does not exist: ${args.from}`,
|
|
4999
|
+
"Check the path and re-run."
|
|
5000
|
+
]
|
|
5001
|
+
};
|
|
5002
|
+
}
|
|
5003
|
+
const systemDirs = [
|
|
5004
|
+
"_memory",
|
|
5005
|
+
"worklog",
|
|
5006
|
+
"decision-log",
|
|
5007
|
+
"runbooks",
|
|
5008
|
+
"hubs",
|
|
5009
|
+
"inbox"
|
|
5010
|
+
];
|
|
5011
|
+
const systemDirsCreated = [];
|
|
5012
|
+
if (!args.dryRun) {
|
|
5013
|
+
for (const d2 of systemDirs) {
|
|
5014
|
+
const p = join22(dataDir, d2);
|
|
5015
|
+
if (!existsSync8(p)) {
|
|
5016
|
+
await mkdir5(p, { recursive: true });
|
|
5017
|
+
systemDirsCreated.push(d2);
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
}
|
|
5021
|
+
const stats = {
|
|
5022
|
+
totalFiles: 0,
|
|
5023
|
+
copied: 0,
|
|
5024
|
+
classified: { ...emptyClassified },
|
|
5025
|
+
frontmatterInjected: 0,
|
|
5026
|
+
frontmatterPreserved: 0,
|
|
5027
|
+
skipped: 0
|
|
5028
|
+
};
|
|
5029
|
+
await walkAndImport(args.from, args.from, dataDir, args.dryRun ?? false, stats);
|
|
5030
|
+
let links;
|
|
5031
|
+
if (!args.dryRun && stats.copied > 0) {
|
|
5032
|
+
try {
|
|
5033
|
+
const check = await checkDirectory(dataDir, { caseInsensitive: true });
|
|
5034
|
+
links = {
|
|
5035
|
+
filesScanned: check.filesScanned,
|
|
5036
|
+
total: check.totalLinks,
|
|
5037
|
+
resolved: check.resolved,
|
|
5038
|
+
broken: check.broken.length,
|
|
5039
|
+
ambiguous: check.ambiguous.length
|
|
5040
|
+
};
|
|
5041
|
+
} catch {
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
const nextActions = [];
|
|
5045
|
+
if (args.dryRun) {
|
|
5046
|
+
nextActions.push("This was a dry-run \u2014 no files were copied. Re-run without --dry-run to apply.");
|
|
5047
|
+
} else {
|
|
5048
|
+
nextActions.push("Run `/vortex status` to see the new counts.");
|
|
5049
|
+
if (stats.classified.preserved > 0) {
|
|
5050
|
+
nextActions.push(`${stats.classified.preserved} files preserved your original folder structure (under <dataDir>/<same-paths>).`);
|
|
5051
|
+
}
|
|
5052
|
+
const autoClassified = stats.classified.worklog + stats.classified.decisionLog + stats.classified.runbooks + stats.classified.hubs + stats.classified.memory;
|
|
5053
|
+
if (autoClassified > 0) {
|
|
5054
|
+
nextActions.push(`${autoClassified} files auto-classified into vortex categories (worklog/decision-log/runbooks/hubs/_memory).`);
|
|
5055
|
+
}
|
|
5056
|
+
if (links && (links.broken > 0 || links.ambiguous > 0)) {
|
|
5057
|
+
nextActions.push(`Wiki-link health: ${links.resolved}/${links.total} resolved across ${links.filesScanned} files; ${links.broken} broken, ${links.ambiguous} ambiguous. Review and curate links over time.`);
|
|
5058
|
+
} else if (links && links.total > 0) {
|
|
5059
|
+
nextActions.push(`Wiki-link health: ${links.resolved}/${links.total} resolved across ${links.filesScanned} files \u2014 all clean.`);
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
return {
|
|
5063
|
+
subcommand: "import",
|
|
5064
|
+
status: args.dryRun ? "dry-run" : "completed",
|
|
5065
|
+
source: args.from,
|
|
5066
|
+
totalFiles: stats.totalFiles,
|
|
5067
|
+
copied: stats.copied,
|
|
5068
|
+
classified: stats.classified,
|
|
5069
|
+
frontmatterInjected: stats.frontmatterInjected,
|
|
5070
|
+
frontmatterPreserved: stats.frontmatterPreserved,
|
|
5071
|
+
systemDirsCreated,
|
|
5072
|
+
skipped: stats.skipped,
|
|
5073
|
+
links,
|
|
5074
|
+
nextActions
|
|
5075
|
+
};
|
|
5076
|
+
}
|
|
5077
|
+
async function walkAndImport(rootSource, currentDir, dataDir, dryRun, stats) {
|
|
5078
|
+
const entries = await readdir15(currentDir, { withFileTypes: true });
|
|
5079
|
+
for (const e of entries) {
|
|
5080
|
+
const sourcePath = join22(currentDir, e.name);
|
|
5081
|
+
if (e.isDirectory()) {
|
|
5082
|
+
if (IMPORT_SKIP_DIRS.has(e.name.toLowerCase()))
|
|
5083
|
+
continue;
|
|
5084
|
+
await walkAndImport(rootSource, sourcePath, dataDir, dryRun, stats);
|
|
5085
|
+
} else if (e.isFile()) {
|
|
5086
|
+
if (IMPORT_SKIP_FILES.has(e.name)) {
|
|
5087
|
+
stats.skipped++;
|
|
5088
|
+
continue;
|
|
5089
|
+
}
|
|
5090
|
+
if (!e.name.endsWith(".md")) {
|
|
5091
|
+
stats.skipped++;
|
|
5092
|
+
continue;
|
|
5093
|
+
}
|
|
5094
|
+
stats.totalFiles++;
|
|
5095
|
+
const raw = await readFile17(sourcePath, "utf8");
|
|
5096
|
+
const parsed = parseFrontmatter(raw);
|
|
5097
|
+
const hasFrontmatter = Object.keys(parsed.frontmatter).length > 0;
|
|
5098
|
+
const category = classifyFile(sourcePath, rootSource, e.name, parsed.frontmatter);
|
|
5099
|
+
stats.classified[category]++;
|
|
5100
|
+
if (hasFrontmatter) {
|
|
5101
|
+
stats.frontmatterPreserved++;
|
|
5102
|
+
} else {
|
|
5103
|
+
stats.frontmatterInjected++;
|
|
5104
|
+
}
|
|
5105
|
+
if (!dryRun) {
|
|
5106
|
+
const fileStat = await stat8(sourcePath);
|
|
5107
|
+
const enhanced = enhanceFrontmatter(parsed.frontmatter, category, fileStat.birthtime, fileStat.mtime, sourcePath, rootSource);
|
|
5108
|
+
const targetPath = computeTargetPath(category, sourcePath, rootSource, dataDir, e.name);
|
|
5109
|
+
await mkdir5(dirname4(targetPath), { recursive: true });
|
|
5110
|
+
const out = serializeFrontmatter({
|
|
5111
|
+
frontmatter: enhanced,
|
|
5112
|
+
body: parsed.body
|
|
5113
|
+
});
|
|
5114
|
+
await writeFile10(targetPath, out, "utf8");
|
|
5115
|
+
stats.copied++;
|
|
5116
|
+
}
|
|
5117
|
+
}
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
function classifyFile(sourcePath, rootSource, filename, frontmatter) {
|
|
5121
|
+
const type = String(frontmatter.type ?? "").toLowerCase();
|
|
5122
|
+
if (type === "worklog" || LEGACY_WORKLOG_TYPES.has(type))
|
|
5123
|
+
return "worklog";
|
|
5124
|
+
if (type === "decision-log" || type === "decision")
|
|
5125
|
+
return "decisionLog";
|
|
5126
|
+
if (type === "runbook")
|
|
5127
|
+
return "runbooks";
|
|
5128
|
+
if (type === "hub")
|
|
5129
|
+
return "hubs";
|
|
5130
|
+
if (type === "memory" || type === "user")
|
|
5131
|
+
return "memory";
|
|
5132
|
+
if (filename.startsWith("_HUB-"))
|
|
5133
|
+
return "hubs";
|
|
5134
|
+
if (FILENAME_DATE_PATTERN.test(filename))
|
|
5135
|
+
return "worklog";
|
|
5136
|
+
const relPath = sourcePath.substring(rootSource.length).replace(/^[/\\]/, "");
|
|
5137
|
+
const parts = relPath.split(/[/\\]/).map((p) => p.toLowerCase());
|
|
5138
|
+
for (const part of parts) {
|
|
5139
|
+
if (WORKLOG_FOLDER_NAMES.has(part))
|
|
5140
|
+
return "worklog";
|
|
5141
|
+
if (DECISION_FOLDER_NAMES.has(part))
|
|
5142
|
+
return "decisionLog";
|
|
5143
|
+
if (RUNBOOK_FOLDER_NAMES.has(part))
|
|
5144
|
+
return "runbooks";
|
|
5145
|
+
if (HUB_FOLDER_NAMES.has(part))
|
|
5146
|
+
return "hubs";
|
|
5147
|
+
if (MEMORY_FOLDER_NAMES.has(part))
|
|
5148
|
+
return "memory";
|
|
5149
|
+
}
|
|
5150
|
+
return "preserved";
|
|
5151
|
+
}
|
|
5152
|
+
function computeTargetPath(category, sourcePath, rootSource, dataDir, filename) {
|
|
5153
|
+
if (category === "preserved") {
|
|
5154
|
+
const relPath = sourcePath.substring(rootSource.length).replace(/^[/\\]/, "");
|
|
5155
|
+
return join22(dataDir, relPath);
|
|
5156
|
+
}
|
|
5157
|
+
if (category === "worklog") {
|
|
5158
|
+
const match = filename.match(/^(\d{4})-(\d{2})-/);
|
|
5159
|
+
if (match) {
|
|
5160
|
+
return join22(dataDir, "worklog", match[1], match[2], filename);
|
|
5161
|
+
}
|
|
5162
|
+
const d2 = /* @__PURE__ */ new Date();
|
|
5163
|
+
const y2 = String(d2.getFullYear());
|
|
5164
|
+
const m2 = String(d2.getMonth() + 1).padStart(2, "0");
|
|
5165
|
+
return join22(dataDir, "worklog", y2, m2, filename);
|
|
5166
|
+
}
|
|
5167
|
+
if (category === "decisionLog")
|
|
5168
|
+
return join22(dataDir, "decision-log", filename);
|
|
5169
|
+
if (category === "runbooks")
|
|
5170
|
+
return join22(dataDir, "runbooks", filename);
|
|
5171
|
+
if (category === "hubs")
|
|
5172
|
+
return join22(dataDir, "hubs", filename);
|
|
5173
|
+
if (category === "memory")
|
|
5174
|
+
return join22(dataDir, "_memory", filename);
|
|
5175
|
+
return join22(dataDir, filename);
|
|
5176
|
+
}
|
|
5177
|
+
function enhanceFrontmatter(frontmatter, category, birthtime, mtime, sourcePath, rootSource) {
|
|
5178
|
+
const enhanced = { ...frontmatter };
|
|
5179
|
+
const existingType = String(enhanced.type ?? "").toLowerCase();
|
|
5180
|
+
if (!existingType) {
|
|
5181
|
+
const typeMap = {
|
|
5182
|
+
worklog: "worklog",
|
|
5183
|
+
decisionLog: "decision-log",
|
|
5184
|
+
runbooks: "runbook",
|
|
5185
|
+
hubs: "hub",
|
|
5186
|
+
memory: "memory",
|
|
5187
|
+
preserved: "note"
|
|
5188
|
+
};
|
|
5189
|
+
enhanced.type = typeMap[category];
|
|
5190
|
+
} else if (LEGACY_WORKLOG_TYPES.has(existingType)) {
|
|
5191
|
+
enhanced.type = "worklog";
|
|
5192
|
+
}
|
|
5193
|
+
if (Array.isArray(enhanced.tags)) {
|
|
5194
|
+
enhanced.tags = enhanced.tags.map((t) => {
|
|
5195
|
+
const s = String(t).toLowerCase();
|
|
5196
|
+
if (LEGACY_WORKLOG_TYPES.has(s))
|
|
5197
|
+
return "worklog";
|
|
5198
|
+
return t;
|
|
5199
|
+
});
|
|
5200
|
+
}
|
|
5201
|
+
if (!enhanced.created) {
|
|
5202
|
+
enhanced.created = formatYmd3(birthtime);
|
|
5203
|
+
}
|
|
5204
|
+
if (!enhanced.updated) {
|
|
5205
|
+
enhanced.updated = formatYmd3(mtime);
|
|
5206
|
+
}
|
|
5207
|
+
if (!enhanced.privacy) {
|
|
5208
|
+
const relLower = sourcePath.substring(rootSource.length).toLowerCase();
|
|
5209
|
+
if (relLower.includes("personal-records") || relLower.includes("personal_records")) {
|
|
5210
|
+
enhanced.privacy = "personal";
|
|
5211
|
+
} else {
|
|
5212
|
+
enhanced.privacy = "internal";
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
return enhanced;
|
|
5216
|
+
}
|
|
5217
|
+
function formatYmd3(d2) {
|
|
2960
5218
|
const y2 = d2.getFullYear();
|
|
2961
5219
|
const m2 = String(d2.getMonth() + 1).padStart(2, "0");
|
|
2962
5220
|
const day = String(d2.getDate()).padStart(2, "0");
|
|
2963
5221
|
return `${y2}-${m2}-${day}`;
|
|
2964
5222
|
}
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
5223
|
+
var DOCTOR_SYSTEM_DIRS = [
|
|
5224
|
+
"_memory",
|
|
5225
|
+
"worklog",
|
|
5226
|
+
"decision-log",
|
|
5227
|
+
"runbooks",
|
|
5228
|
+
"hubs"
|
|
5229
|
+
];
|
|
5230
|
+
var RUNBOOK_AGING_DAYS = 90;
|
|
5231
|
+
var NODE_MIN_MAJOR = 18;
|
|
5232
|
+
async function runDoctor(input) {
|
|
5233
|
+
const { dataDir, repoRoot } = input.context;
|
|
5234
|
+
const checks = [];
|
|
5235
|
+
checks.push(checkSystemDirs(dataDir));
|
|
5236
|
+
checks.push(checkUserProfile(dataDir));
|
|
5237
|
+
checks.push(await checkIndexes(dataDir));
|
|
5238
|
+
checks.push(await checkWikilinks(dataDir));
|
|
5239
|
+
checks.push(await checkFrontmatterLint(dataDir));
|
|
5240
|
+
checks.push(await checkRunbookAging(dataDir));
|
|
5241
|
+
checks.push(checkNodeVersion());
|
|
5242
|
+
checks.push(await checkGitRemote(repoRoot));
|
|
5243
|
+
const summary = { pass: 0, warn: 0, fail: 0, info: 0 };
|
|
5244
|
+
for (const c of checks)
|
|
5245
|
+
summary[c.status]++;
|
|
5246
|
+
const status = summary.fail > 0 ? "errors" : summary.warn > 0 ? "warnings" : "ok";
|
|
5247
|
+
const nextActions = [];
|
|
5248
|
+
if (summary.fail > 0) {
|
|
5249
|
+
nextActions.push(`${summary.fail} check(s) failed. Address them before relying on the instance.`);
|
|
5250
|
+
}
|
|
5251
|
+
if (summary.warn > 0) {
|
|
5252
|
+
nextActions.push(`${summary.warn} warning(s). Review the detail lines \u2014 most are recoverable in one command.`);
|
|
5253
|
+
}
|
|
5254
|
+
if (status === "ok") {
|
|
5255
|
+
nextActions.push("All checks pass. Run `/vortex status` for activity counts or `/log <section>` to keep working.");
|
|
5256
|
+
}
|
|
5257
|
+
return { subcommand: "doctor", status, checks, summary, nextActions };
|
|
5258
|
+
}
|
|
5259
|
+
function checkSystemDirs(dataDir) {
|
|
5260
|
+
const missing = DOCTOR_SYSTEM_DIRS.filter((d2) => !existsSync8(join22(dataDir, d2)));
|
|
5261
|
+
if (missing.length === 0) {
|
|
5262
|
+
return {
|
|
5263
|
+
id: "system-dirs",
|
|
5264
|
+
label: "vortex system directories present",
|
|
5265
|
+
status: "pass"
|
|
5266
|
+
};
|
|
5267
|
+
}
|
|
5268
|
+
return {
|
|
5269
|
+
id: "system-dirs",
|
|
5270
|
+
label: "vortex system directories present",
|
|
5271
|
+
status: "fail",
|
|
5272
|
+
detail: `Missing: ${missing.join(", ")}. Run \`/vortex init\` or create them manually.`
|
|
5273
|
+
};
|
|
5274
|
+
}
|
|
5275
|
+
function checkUserProfile(dataDir) {
|
|
5276
|
+
const profilePath = join22(dataDir, "_memory", "user_profile.md");
|
|
5277
|
+
if (existsSync8(profilePath)) {
|
|
5278
|
+
return {
|
|
5279
|
+
id: "user-profile",
|
|
5280
|
+
label: "user_profile.md exists",
|
|
5281
|
+
status: "pass"
|
|
5282
|
+
};
|
|
5283
|
+
}
|
|
5284
|
+
return {
|
|
5285
|
+
id: "user-profile",
|
|
5286
|
+
label: "user_profile.md exists",
|
|
5287
|
+
status: "fail",
|
|
5288
|
+
detail: "Missing _memory/user_profile.md. Run `/vortex init` to bootstrap the instance."
|
|
5289
|
+
};
|
|
5290
|
+
}
|
|
5291
|
+
async function checkIndexes(dataDir) {
|
|
5292
|
+
const missing = [];
|
|
5293
|
+
for (const d2 of DOCTOR_SYSTEM_DIRS) {
|
|
5294
|
+
const dirPath = join22(dataDir, d2);
|
|
5295
|
+
if (!existsSync8(dirPath))
|
|
5296
|
+
continue;
|
|
5297
|
+
const indexPath = join22(dirPath, "_INDEX.md");
|
|
5298
|
+
if (!existsSync8(indexPath))
|
|
5299
|
+
missing.push(`${d2}/_INDEX.md`);
|
|
5300
|
+
}
|
|
5301
|
+
if (missing.length === 0) {
|
|
5302
|
+
return {
|
|
5303
|
+
id: "indexes",
|
|
5304
|
+
label: "_INDEX.md present in each system directory",
|
|
5305
|
+
status: "pass"
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5308
|
+
return {
|
|
5309
|
+
id: "indexes",
|
|
5310
|
+
label: "_INDEX.md present in each system directory",
|
|
5311
|
+
status: "warn",
|
|
5312
|
+
detail: `Missing: ${missing.join(", ")}. Run \`/reindex\` to generate them.`
|
|
5313
|
+
};
|
|
5314
|
+
}
|
|
5315
|
+
async function checkWikilinks(dataDir) {
|
|
5316
|
+
try {
|
|
5317
|
+
const result = await checkDirectory(dataDir, { caseInsensitive: true });
|
|
5318
|
+
if (result.totalLinks === 0) {
|
|
5319
|
+
return {
|
|
5320
|
+
id: "wikilinks",
|
|
5321
|
+
label: "wiki links resolve",
|
|
5322
|
+
status: "pass",
|
|
5323
|
+
detail: "No wiki links found."
|
|
5324
|
+
};
|
|
2979
5325
|
}
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
const sub = tokens[0] ?? "help";
|
|
2984
|
-
const restAfterSub = tokens.slice(1);
|
|
2985
|
-
if (sub === "init")
|
|
2986
|
-
return runInit(input, restAfterSub);
|
|
2987
|
-
if (sub === "help" || sub === "")
|
|
2988
|
-
return runHelp();
|
|
2989
|
-
if (PLANNED_SUBS.includes(sub)) {
|
|
5326
|
+
const broken = result.broken.length;
|
|
5327
|
+
const ambiguous = result.ambiguous.length;
|
|
5328
|
+
if (broken === 0 && ambiguous === 0) {
|
|
2990
5329
|
return {
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
5330
|
+
id: "wikilinks",
|
|
5331
|
+
label: "wiki links resolve",
|
|
5332
|
+
status: "pass",
|
|
5333
|
+
detail: `${result.resolved}/${result.totalLinks} resolved across ${result.filesScanned} files.`
|
|
2994
5334
|
};
|
|
2995
5335
|
}
|
|
2996
5336
|
return {
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
5337
|
+
id: "wikilinks",
|
|
5338
|
+
label: "wiki links resolve",
|
|
5339
|
+
status: "warn",
|
|
5340
|
+
detail: `${broken} broken, ${ambiguous} ambiguous out of ${result.totalLinks} total. Curate or run a rewrite pass.`
|
|
5341
|
+
};
|
|
5342
|
+
} catch (e) {
|
|
5343
|
+
return {
|
|
5344
|
+
id: "wikilinks",
|
|
5345
|
+
label: "wiki links resolve",
|
|
5346
|
+
status: "warn",
|
|
5347
|
+
detail: `Could not scan: ${e.message}`
|
|
5348
|
+
};
|
|
5349
|
+
}
|
|
5350
|
+
}
|
|
5351
|
+
async function checkFrontmatterLint(dataDir) {
|
|
5352
|
+
try {
|
|
5353
|
+
const report = await lintDirectory({
|
|
5354
|
+
dir: dataDir,
|
|
5355
|
+
rules: [
|
|
5356
|
+
requireFrontmatter({ required: ["type"] }),
|
|
5357
|
+
privacyValid(),
|
|
5358
|
+
wikiLinkResolves({ searchRoot: dataDir })
|
|
5359
|
+
]
|
|
5360
|
+
});
|
|
5361
|
+
const errors = report.findings.filter((f) => f.severity === "error").length;
|
|
5362
|
+
const warnings = report.findings.filter((f) => f.severity === "warning").length;
|
|
5363
|
+
if (errors === 0 && warnings === 0) {
|
|
5364
|
+
return {
|
|
5365
|
+
id: "frontmatter-lint",
|
|
5366
|
+
label: "frontmatter / privacy / wiki-link rules",
|
|
5367
|
+
status: "pass",
|
|
5368
|
+
detail: `${report.filesScanned} files scanned, 0 findings.`
|
|
5369
|
+
};
|
|
5370
|
+
}
|
|
5371
|
+
if (errors > 0) {
|
|
5372
|
+
return {
|
|
5373
|
+
id: "frontmatter-lint",
|
|
5374
|
+
label: "frontmatter / privacy / wiki-link rules",
|
|
5375
|
+
status: "fail",
|
|
5376
|
+
detail: `${errors} error(s), ${warnings} warning(s) across ${report.filesScanned} files.`
|
|
5377
|
+
};
|
|
5378
|
+
}
|
|
5379
|
+
return {
|
|
5380
|
+
id: "frontmatter-lint",
|
|
5381
|
+
label: "frontmatter / privacy / wiki-link rules",
|
|
5382
|
+
status: "warn",
|
|
5383
|
+
detail: `${warnings} warning(s) across ${report.filesScanned} files.`
|
|
5384
|
+
};
|
|
5385
|
+
} catch (e) {
|
|
5386
|
+
return {
|
|
5387
|
+
id: "frontmatter-lint",
|
|
5388
|
+
label: "frontmatter / privacy / wiki-link rules",
|
|
5389
|
+
status: "warn",
|
|
5390
|
+
detail: `Could not lint: ${e.message}`
|
|
5391
|
+
};
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5394
|
+
async function checkRunbookAging(dataDir) {
|
|
5395
|
+
const runbooksDir = join22(dataDir, "runbooks");
|
|
5396
|
+
if (!existsSync8(runbooksDir)) {
|
|
5397
|
+
return {
|
|
5398
|
+
id: "runbook-aging",
|
|
5399
|
+
label: `runbooks tested within ${RUNBOOK_AGING_DAYS} days`,
|
|
5400
|
+
status: "pass",
|
|
5401
|
+
detail: "No runbooks directory."
|
|
5402
|
+
};
|
|
5403
|
+
}
|
|
5404
|
+
const stale = [];
|
|
5405
|
+
let total = 0;
|
|
5406
|
+
const cutoff = Date.now() - RUNBOOK_AGING_DAYS * 24 * 60 * 60 * 1e3;
|
|
5407
|
+
try {
|
|
5408
|
+
const entries = await readdir15(runbooksDir, { withFileTypes: true });
|
|
5409
|
+
for (const e of entries) {
|
|
5410
|
+
if (!e.isFile() || !e.name.endsWith(".md"))
|
|
5411
|
+
continue;
|
|
5412
|
+
if (e.name === "README.md" || e.name === "_INDEX.md" || e.name.startsWith("_TEMPLATE")) {
|
|
5413
|
+
continue;
|
|
5414
|
+
}
|
|
5415
|
+
total++;
|
|
5416
|
+
const filePath = join22(runbooksDir, e.name);
|
|
5417
|
+
const raw = await readFile17(filePath, "utf8");
|
|
5418
|
+
const { frontmatter } = parseFrontmatter(raw);
|
|
5419
|
+
if (!frontmatter.last_tested) {
|
|
5420
|
+
stale.push(`${e.name} (no last_tested)`);
|
|
5421
|
+
continue;
|
|
5422
|
+
}
|
|
5423
|
+
const testedAt = new Date(String(frontmatter.last_tested)).getTime();
|
|
5424
|
+
if (Number.isNaN(testedAt) || testedAt < cutoff) {
|
|
5425
|
+
stale.push(`${e.name} (${String(frontmatter.last_tested)})`);
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
} catch (e) {
|
|
5429
|
+
return {
|
|
5430
|
+
id: "runbook-aging",
|
|
5431
|
+
label: `runbooks tested within ${RUNBOOK_AGING_DAYS} days`,
|
|
5432
|
+
status: "warn",
|
|
5433
|
+
detail: `Could not scan: ${e.message}`
|
|
5434
|
+
};
|
|
5435
|
+
}
|
|
5436
|
+
if (total === 0 || stale.length === 0) {
|
|
5437
|
+
return {
|
|
5438
|
+
id: "runbook-aging",
|
|
5439
|
+
label: `runbooks tested within ${RUNBOOK_AGING_DAYS} days`,
|
|
5440
|
+
status: "pass",
|
|
5441
|
+
detail: total === 0 ? "No runbooks." : `${total} runbook(s), all fresh.`
|
|
3000
5442
|
};
|
|
3001
5443
|
}
|
|
3002
|
-
};
|
|
3003
|
-
function runHelp() {
|
|
3004
5444
|
return {
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
name: "init",
|
|
3010
|
-
description: "First-time setup wizard. Creates user profile memory, first TIL, first topic hub.",
|
|
3011
|
-
state: "active"
|
|
3012
|
-
},
|
|
3013
|
-
{
|
|
3014
|
-
name: "status",
|
|
3015
|
-
description: "Show instance state (memory count, latest TIL, missing skeletons).",
|
|
3016
|
-
state: "planned"
|
|
3017
|
-
},
|
|
3018
|
-
{
|
|
3019
|
-
name: "import",
|
|
3020
|
-
description: "Bring an existing folder into data/imported/ for gradual sorting.",
|
|
3021
|
-
state: "planned"
|
|
3022
|
-
},
|
|
3023
|
-
{
|
|
3024
|
-
name: "doctor",
|
|
3025
|
-
description: "Diagnose common setup issues (missing modules, broken links).",
|
|
3026
|
-
state: "planned"
|
|
3027
|
-
},
|
|
3028
|
-
{ name: "help", description: "Show this list.", state: "active" }
|
|
3029
|
-
]
|
|
5445
|
+
id: "runbook-aging",
|
|
5446
|
+
label: `runbooks tested within ${RUNBOOK_AGING_DAYS} days`,
|
|
5447
|
+
status: "warn",
|
|
5448
|
+
detail: `${stale.length}/${total} runbook(s) stale or untested: ${stale.slice(0, 3).join("; ")}${stale.length > 3 ? "\u2026" : ""}. Even rarely-used runbooks stay valuable \u2014 re-verify when the environment changes, do not delete.`
|
|
3030
5449
|
};
|
|
3031
5450
|
}
|
|
3032
|
-
|
|
3033
|
-
const
|
|
3034
|
-
const
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
5451
|
+
function checkNodeVersion() {
|
|
5452
|
+
const raw = process.version;
|
|
5453
|
+
const match = raw.match(/^v?(\d+)\./);
|
|
5454
|
+
if (!match) {
|
|
5455
|
+
return {
|
|
5456
|
+
id: "node-version",
|
|
5457
|
+
label: `node >= ${NODE_MIN_MAJOR}`,
|
|
5458
|
+
status: "warn",
|
|
5459
|
+
detail: `Could not parse node version "${raw}".`
|
|
5460
|
+
};
|
|
3040
5461
|
}
|
|
3041
|
-
const
|
|
3042
|
-
if (
|
|
5462
|
+
const major = Number.parseInt(match[1], 10);
|
|
5463
|
+
if (major >= NODE_MIN_MAJOR) {
|
|
3043
5464
|
return {
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
`VortEX instance is already initialized (${profilePath} exists).`,
|
|
3049
|
-
"To re-run, pass `--force` (existing user_profile / first TIL / first hub will be overwritten).",
|
|
3050
|
-
"To check current state, try `/session-start`."
|
|
3051
|
-
]
|
|
5465
|
+
id: "node-version",
|
|
5466
|
+
label: `node >= ${NODE_MIN_MAJOR}`,
|
|
5467
|
+
status: "pass",
|
|
5468
|
+
detail: `Running ${raw}.`
|
|
3052
5469
|
};
|
|
3053
5470
|
}
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
5471
|
+
return {
|
|
5472
|
+
id: "node-version",
|
|
5473
|
+
label: `node >= ${NODE_MIN_MAJOR}`,
|
|
5474
|
+
status: "fail",
|
|
5475
|
+
detail: `Found ${raw}. TypeScript modules require node ${NODE_MIN_MAJOR} or later. Upgrade node.`
|
|
5476
|
+
};
|
|
5477
|
+
}
|
|
5478
|
+
async function checkGitRemote(repoRoot) {
|
|
5479
|
+
const gitConfig = join22(repoRoot, ".git", "config");
|
|
5480
|
+
if (!existsSync8(gitConfig)) {
|
|
5481
|
+
return {
|
|
5482
|
+
id: "git-remote",
|
|
5483
|
+
label: "git remote for sync",
|
|
5484
|
+
status: "info",
|
|
5485
|
+
detail: "Not a git repository. VortEX works fine without git, but git + a hosted remote (GitHub, Gitea, GitLab) is recommended if you want to sync across machines (work / home / USB)."
|
|
5486
|
+
};
|
|
3060
5487
|
}
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
5488
|
+
try {
|
|
5489
|
+
const raw = await readFile17(gitConfig, "utf8");
|
|
5490
|
+
const match = raw.match(/\[remote "origin"\][\s\S]*?url\s*=\s*(.+)/);
|
|
5491
|
+
if (!match) {
|
|
5492
|
+
return {
|
|
5493
|
+
id: "git-remote",
|
|
5494
|
+
label: "git remote for sync",
|
|
5495
|
+
status: "info",
|
|
5496
|
+
detail: "Git repo present but no `origin` remote configured. Add one (`git remote add origin <url>`) if you want cross-machine sync."
|
|
5497
|
+
};
|
|
5498
|
+
}
|
|
5499
|
+
return {
|
|
5500
|
+
id: "git-remote",
|
|
5501
|
+
label: "git remote for sync",
|
|
5502
|
+
status: "pass",
|
|
5503
|
+
detail: `origin = ${match[1].trim()}`
|
|
5504
|
+
};
|
|
5505
|
+
} catch (e) {
|
|
5506
|
+
return {
|
|
5507
|
+
id: "git-remote",
|
|
5508
|
+
label: "git remote for sync",
|
|
5509
|
+
status: "info",
|
|
5510
|
+
detail: `Could not read .git/config: ${e.message}`
|
|
5511
|
+
};
|
|
3066
5512
|
}
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
5513
|
+
}
|
|
5514
|
+
async function detectExternalFolders(excludePath) {
|
|
5515
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
5516
|
+
if (!home)
|
|
5517
|
+
return void 0;
|
|
5518
|
+
const candidates = [
|
|
5519
|
+
join22(home, "Documents", "obsidian-vault"),
|
|
5520
|
+
join22(home, "Documents", "notes"),
|
|
5521
|
+
join22(home, "Documents", "Notebook"),
|
|
5522
|
+
join22(home, "notes"),
|
|
5523
|
+
join22(home, "Notes")
|
|
5524
|
+
];
|
|
5525
|
+
const excludeNorm = excludePath.replace(/[/\\]+$/, "");
|
|
5526
|
+
const found = [];
|
|
5527
|
+
for (const candidate of candidates) {
|
|
5528
|
+
const candNorm = candidate.replace(/[/\\]+$/, "");
|
|
5529
|
+
if (candNorm === excludeNorm || candNorm.startsWith(excludeNorm + "/") || candNorm.startsWith(excludeNorm + "\\") || excludeNorm.startsWith(candNorm + "/") || excludeNorm.startsWith(candNorm + "\\")) {
|
|
5530
|
+
continue;
|
|
5531
|
+
}
|
|
5532
|
+
if (!existsSync8(candidate))
|
|
5533
|
+
continue;
|
|
5534
|
+
let mdCount = 0;
|
|
5535
|
+
try {
|
|
5536
|
+
mdCount = await countMarkdown2(candidate, true);
|
|
5537
|
+
} catch {
|
|
5538
|
+
continue;
|
|
5539
|
+
}
|
|
5540
|
+
if (mdCount === 0)
|
|
5541
|
+
continue;
|
|
5542
|
+
found.push({ path: candidate, basename: basename7(candidate), mdCount });
|
|
3072
5543
|
}
|
|
3073
|
-
|
|
5544
|
+
return found.length > 0 ? found : void 0;
|
|
5545
|
+
}
|
|
5546
|
+
function parseSyncArgs(tokens) {
|
|
5547
|
+
const args = {};
|
|
5548
|
+
for (const t of tokens) {
|
|
5549
|
+
if (t === "--skip-pull")
|
|
5550
|
+
args.skipPull = true;
|
|
5551
|
+
else if (t === "--skip-install")
|
|
5552
|
+
args.skipInstall = true;
|
|
5553
|
+
else if (t === "--skip-build")
|
|
5554
|
+
args.skipBuild = true;
|
|
5555
|
+
else if (t === "--skip-verify")
|
|
5556
|
+
args.skipVerify = true;
|
|
5557
|
+
else if (t === "--dry-run")
|
|
5558
|
+
args.dryRun = true;
|
|
5559
|
+
}
|
|
5560
|
+
return args;
|
|
5561
|
+
}
|
|
5562
|
+
async function runSync(input, tokens) {
|
|
5563
|
+
const args = parseSyncArgs(tokens);
|
|
5564
|
+
const { repoRoot } = input.context;
|
|
5565
|
+
const plan = [
|
|
5566
|
+
{
|
|
5567
|
+
id: "pull",
|
|
5568
|
+
label: "git pull",
|
|
5569
|
+
cmd: "git",
|
|
5570
|
+
cmdArgs: ["pull"],
|
|
5571
|
+
skip: args.skipPull ?? false
|
|
5572
|
+
},
|
|
5573
|
+
{
|
|
5574
|
+
id: "install",
|
|
5575
|
+
label: "npm install",
|
|
5576
|
+
cmd: "npm",
|
|
5577
|
+
cmdArgs: ["install"],
|
|
5578
|
+
skip: args.skipInstall ?? false
|
|
5579
|
+
},
|
|
5580
|
+
{
|
|
5581
|
+
id: "build",
|
|
5582
|
+
label: "npm run build",
|
|
5583
|
+
cmd: "npm",
|
|
5584
|
+
cmdArgs: ["run", "build"],
|
|
5585
|
+
skip: args.skipBuild ?? false
|
|
5586
|
+
},
|
|
5587
|
+
{
|
|
5588
|
+
id: "verify",
|
|
5589
|
+
label: "npm run verify",
|
|
5590
|
+
cmd: "npm",
|
|
5591
|
+
cmdArgs: ["run", "verify"],
|
|
5592
|
+
skip: args.skipVerify ?? false
|
|
5593
|
+
}
|
|
5594
|
+
];
|
|
5595
|
+
if (args.dryRun) {
|
|
5596
|
+
const steps2 = plan.map((p) => ({
|
|
5597
|
+
id: p.id,
|
|
5598
|
+
label: p.label,
|
|
5599
|
+
status: p.skip ? "skipped" : "ok"
|
|
5600
|
+
}));
|
|
3074
5601
|
return {
|
|
3075
|
-
subcommand: "
|
|
3076
|
-
status: "
|
|
3077
|
-
|
|
3078
|
-
missingInputs: missing,
|
|
5602
|
+
subcommand: "sync",
|
|
5603
|
+
status: "dry-run",
|
|
5604
|
+
steps: steps2,
|
|
3079
5605
|
nextActions: [
|
|
3080
|
-
"
|
|
3081
|
-
|
|
3082
|
-
"Optional: append `--force` to overwrite an already-initialized instance."
|
|
5606
|
+
"Dry-run only \u2014 no commands executed.",
|
|
5607
|
+
"Re-run without --dry-run to apply."
|
|
3083
5608
|
]
|
|
3084
5609
|
};
|
|
3085
5610
|
}
|
|
3086
|
-
const
|
|
3087
|
-
const
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
5611
|
+
const steps = [];
|
|
5612
|
+
for (const p of plan) {
|
|
5613
|
+
if (p.skip) {
|
|
5614
|
+
steps.push({ id: p.id, label: p.label, status: "skipped" });
|
|
5615
|
+
continue;
|
|
5616
|
+
}
|
|
5617
|
+
const result = await runShellCommand(p.cmd, p.cmdArgs, repoRoot);
|
|
5618
|
+
const step = {
|
|
5619
|
+
id: p.id,
|
|
5620
|
+
label: p.label,
|
|
5621
|
+
status: result.exitCode === 0 ? "ok" : "failed",
|
|
5622
|
+
exitCode: result.exitCode,
|
|
5623
|
+
durationMs: result.durationMs,
|
|
5624
|
+
stdoutTail: result.stdoutTail,
|
|
5625
|
+
stderrTail: result.stderrTail
|
|
5626
|
+
};
|
|
5627
|
+
steps.push(step);
|
|
5628
|
+
if (step.status === "failed") {
|
|
5629
|
+
return {
|
|
5630
|
+
subcommand: "sync",
|
|
5631
|
+
status: "failed",
|
|
5632
|
+
steps,
|
|
5633
|
+
failedAt: p.id,
|
|
5634
|
+
nextActions: [
|
|
5635
|
+
`\`${p.label}\` failed with exit code ${result.exitCode}.`,
|
|
5636
|
+
"Review the stdoutTail / stderrTail and fix the underlying issue, then re-run.",
|
|
5637
|
+
"You can skip already-passed earlier steps with --skip-pull / --skip-install / --skip-build / --skip-verify."
|
|
5638
|
+
]
|
|
5639
|
+
};
|
|
5640
|
+
}
|
|
5641
|
+
}
|
|
5642
|
+
const ranCount = steps.filter((s) => s.status === "ok").length;
|
|
3100
5643
|
return {
|
|
3101
|
-
subcommand: "
|
|
5644
|
+
subcommand: "sync",
|
|
3102
5645
|
status: "completed",
|
|
3103
|
-
|
|
5646
|
+
steps,
|
|
3104
5647
|
nextActions: [
|
|
3105
|
-
`
|
|
3106
|
-
"
|
|
3107
|
-
" /til <one-line update> \u2014 append a section to today's TIL",
|
|
3108
|
-
" /decision <slug> <title> \u2014 record a decision",
|
|
3109
|
-
" /session-start \u2014 daily start-of-session report",
|
|
3110
|
-
`Open ${tilPath} to see your first TIL \u2014 it already names "${args.task}".`
|
|
5648
|
+
`All ${ranCount} step(s) passed. Your framework checkout is up-to-date and verified.`,
|
|
5649
|
+
"Run `/vortex status` for an instance-side snapshot."
|
|
3111
5650
|
]
|
|
3112
5651
|
};
|
|
3113
5652
|
}
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
5653
|
+
var SYNC_TAIL_LENGTH = 1e3;
|
|
5654
|
+
async function runShellCommand(cmd, cmdArgs, cwd) {
|
|
5655
|
+
const start = Date.now();
|
|
5656
|
+
return new Promise((resolve2) => {
|
|
5657
|
+
let stdout = "";
|
|
5658
|
+
let stderr = "";
|
|
5659
|
+
const child = spawn(cmd, [...cmdArgs], { cwd, shell: true });
|
|
5660
|
+
child.stdout?.on("data", (chunk) => {
|
|
5661
|
+
stdout += chunk.toString("utf8");
|
|
5662
|
+
});
|
|
5663
|
+
child.stderr?.on("data", (chunk) => {
|
|
5664
|
+
stderr += chunk.toString("utf8");
|
|
5665
|
+
});
|
|
5666
|
+
child.on("close", (code) => {
|
|
5667
|
+
resolve2({
|
|
5668
|
+
exitCode: code ?? -1,
|
|
5669
|
+
durationMs: Date.now() - start,
|
|
5670
|
+
stdoutTail: tailString(stdout, SYNC_TAIL_LENGTH),
|
|
5671
|
+
stderrTail: tailString(stderr, SYNC_TAIL_LENGTH)
|
|
5672
|
+
});
|
|
5673
|
+
});
|
|
5674
|
+
child.on("error", (err) => {
|
|
5675
|
+
resolve2({
|
|
5676
|
+
exitCode: -1,
|
|
5677
|
+
durationMs: Date.now() - start,
|
|
5678
|
+
stdoutTail: tailString(stdout, SYNC_TAIL_LENGTH),
|
|
5679
|
+
stderrTail: tailString(stderr + "\n[spawn error] " + err.message, SYNC_TAIL_LENGTH)
|
|
5680
|
+
});
|
|
5681
|
+
});
|
|
5682
|
+
});
|
|
5683
|
+
}
|
|
5684
|
+
function tailString(s, n) {
|
|
5685
|
+
if (s.length <= n)
|
|
5686
|
+
return s;
|
|
5687
|
+
return "..." + s.slice(-n);
|
|
5688
|
+
}
|
|
5689
|
+
|
|
5690
|
+
// ../plugins/session-rituals/dist/agenda.js
|
|
5691
|
+
var DEFAULT_RECENT = 7;
|
|
5692
|
+
var DEFAULT_MAX = 8;
|
|
5693
|
+
function worklogTitle(entry) {
|
|
5694
|
+
const m2 = entry.body.match(/^#\s+(.+)$/m);
|
|
5695
|
+
if (m2)
|
|
5696
|
+
return m2[1].trim();
|
|
5697
|
+
return entry.keyword || entry.date;
|
|
5698
|
+
}
|
|
5699
|
+
function extractNextUp(body, max = 8) {
|
|
5700
|
+
const lines = body.split(/\r?\n/);
|
|
5701
|
+
const headingRe = /^(#{1,6})\s+(.*)$/;
|
|
5702
|
+
const cueRe = /(다음\s*작업|다음\s*세션|후속|next\s*up|next|todo|to-do|📋)/i;
|
|
5703
|
+
let collecting = false;
|
|
5704
|
+
let startLevel = 0;
|
|
5705
|
+
const out = [];
|
|
5706
|
+
for (const line of lines) {
|
|
5707
|
+
const h = line.match(headingRe);
|
|
5708
|
+
if (h) {
|
|
5709
|
+
const level = h[1].length;
|
|
5710
|
+
if (collecting && level <= startLevel)
|
|
5711
|
+
break;
|
|
5712
|
+
if (!collecting && cueRe.test(h[2])) {
|
|
5713
|
+
collecting = true;
|
|
5714
|
+
startLevel = level;
|
|
5715
|
+
continue;
|
|
5716
|
+
}
|
|
3120
5717
|
continue;
|
|
3121
5718
|
}
|
|
3122
|
-
if (
|
|
3123
|
-
args.name = tokens[++i];
|
|
5719
|
+
if (!collecting)
|
|
3124
5720
|
continue;
|
|
3125
|
-
|
|
3126
|
-
if (
|
|
3127
|
-
args.role = tokens[++i];
|
|
5721
|
+
const trimmed = line.trim();
|
|
5722
|
+
if (trimmed.length === 0)
|
|
3128
5723
|
continue;
|
|
3129
|
-
|
|
3130
|
-
if (t === "--task" && i + 1 < tokens.length) {
|
|
3131
|
-
args.task = tokens[++i];
|
|
5724
|
+
if (trimmed.startsWith(">"))
|
|
3132
5725
|
continue;
|
|
3133
|
-
|
|
5726
|
+
const cleaned = trimmed.replace(/^[-*]\s+\[[ xX]\]\s+/, "").replace(/^[-*]\s+/, "").replace(/^\d+[.)]\s+/, "").trim();
|
|
5727
|
+
if (cleaned.length === 0)
|
|
5728
|
+
continue;
|
|
5729
|
+
out.push(cleaned);
|
|
5730
|
+
if (out.length >= max)
|
|
5731
|
+
break;
|
|
3134
5732
|
}
|
|
3135
|
-
return
|
|
5733
|
+
return out;
|
|
3136
5734
|
}
|
|
3137
|
-
function
|
|
5735
|
+
function extractOpenTasks(body) {
|
|
3138
5736
|
const out = [];
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
5737
|
+
for (const line of body.split(/\r?\n/)) {
|
|
5738
|
+
const m2 = line.match(/^\s*[-*]\s+\[\s\]\s+(.+\S)\s*$/);
|
|
5739
|
+
if (m2)
|
|
5740
|
+
out.push(m2[1].trim());
|
|
5741
|
+
}
|
|
5742
|
+
return out;
|
|
5743
|
+
}
|
|
5744
|
+
async function collectAgenda(ctx, opts) {
|
|
5745
|
+
const recentN = opts?.recentWorklogs ?? DEFAULT_RECENT;
|
|
5746
|
+
const maxTasks = opts?.maxTasks ?? DEFAULT_MAX;
|
|
5747
|
+
const maxDecisions = opts?.maxDecisions ?? DEFAULT_MAX;
|
|
5748
|
+
const worklogStore = new WorklogStore(`${ctx.dataDir}/worklog`);
|
|
5749
|
+
const decisionStore = new DecisionStore(joinDecisionRoot(ctx));
|
|
5750
|
+
const allWorklogs = await worklogStore.list();
|
|
5751
|
+
const sortedWorklogs = [...allWorklogs].sort((a, b2) => a.date < b2.date ? 1 : a.date > b2.date ? -1 : 0);
|
|
5752
|
+
const recent = sortedWorklogs.slice(0, recentN);
|
|
5753
|
+
const lastWorklog = sortedWorklogs[0] ? { date: sortedWorklogs[0].date, title: worklogTitle(sortedWorklogs[0]), path: sortedWorklogs[0].path } : null;
|
|
5754
|
+
const openTasks = [];
|
|
5755
|
+
for (const wl of recent) {
|
|
5756
|
+
for (const text of extractOpenTasks(wl.body)) {
|
|
5757
|
+
openTasks.push({ text, fromDate: wl.date });
|
|
5758
|
+
if (openTasks.length >= maxTasks)
|
|
5759
|
+
break;
|
|
5760
|
+
}
|
|
5761
|
+
if (openTasks.length >= maxTasks)
|
|
3144
5762
|
break;
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
5763
|
+
}
|
|
5764
|
+
const newest = sortedWorklogs[0];
|
|
5765
|
+
const nextUp = newest ? extractNextUp(newest.body, maxTasks) : [];
|
|
5766
|
+
const nextUpFrom = newest && nextUp.length > 0 ? newest.date : null;
|
|
5767
|
+
const allDecisions = await decisionStore.list();
|
|
5768
|
+
const active = allDecisions.filter((d2) => {
|
|
5769
|
+
const s = (d2.frontmatter?.status ?? "active").toLowerCase();
|
|
5770
|
+
return s !== "archived" && s !== "template";
|
|
5771
|
+
});
|
|
5772
|
+
const sortedDecisions = [...active].sort((a, b2) => a.date < b2.date ? 1 : a.date > b2.date ? -1 : 0);
|
|
5773
|
+
const openDecisions = sortedDecisions.slice(0, maxDecisions).map((d2) => ({
|
|
5774
|
+
title: decisionTitle(d2),
|
|
5775
|
+
date: d2.date,
|
|
5776
|
+
slug: d2.slug
|
|
5777
|
+
}));
|
|
5778
|
+
const worklogCount = allWorklogs.length;
|
|
5779
|
+
const decisionCount = allDecisions.length;
|
|
5780
|
+
const isEmpty = worklogCount === 0 && decisionCount === 0;
|
|
5781
|
+
const nothingOpen = !isEmpty && openTasks.length === 0 && openDecisions.length === 0 && nextUp.length === 0;
|
|
5782
|
+
return {
|
|
5783
|
+
lastWorklog,
|
|
5784
|
+
nextUp,
|
|
5785
|
+
nextUpFrom,
|
|
5786
|
+
openTasks,
|
|
5787
|
+
openDecisions,
|
|
5788
|
+
worklogCount,
|
|
5789
|
+
decisionCount,
|
|
5790
|
+
isEmpty,
|
|
5791
|
+
nothingOpen
|
|
5792
|
+
};
|
|
5793
|
+
}
|
|
5794
|
+
function joinDecisionRoot(ctx) {
|
|
5795
|
+
return `${ctx.dataDir}/decision-log`;
|
|
5796
|
+
}
|
|
5797
|
+
function decisionTitle(d2) {
|
|
5798
|
+
const m2 = (d2.body ?? "").match(/^#\s+(.+)$/m);
|
|
5799
|
+
if (m2)
|
|
5800
|
+
return m2[1].trim();
|
|
5801
|
+
return d2.slug;
|
|
5802
|
+
}
|
|
5803
|
+
function renderAgenda(report) {
|
|
5804
|
+
const lines = ["## What should I do today?", ""];
|
|
5805
|
+
if (report.isEmpty) {
|
|
5806
|
+
lines.push("- No worklog or decisions yet \u2014 this looks like a fresh instance.");
|
|
5807
|
+
lines.push("- Start with `/vortex init` (if you haven't), then `/log <one-line update>` as you work.");
|
|
5808
|
+
lines.push("- A worklog entry per working day is the seed; everything else grows from it.");
|
|
5809
|
+
return lines.join("\n") + "\n";
|
|
5810
|
+
}
|
|
5811
|
+
if (report.lastWorklog) {
|
|
5812
|
+
lines.push(`- last active: ${report.lastWorklog.date} \u2014 ${report.lastWorklog.title}`);
|
|
5813
|
+
}
|
|
5814
|
+
if (report.nextUp.length > 0) {
|
|
5815
|
+
lines.push(`- next up (planned, from ${report.nextUpFrom}):`);
|
|
5816
|
+
for (const n of report.nextUp) {
|
|
5817
|
+
lines.push(` - ${n}`);
|
|
3162
5818
|
}
|
|
3163
5819
|
}
|
|
3164
|
-
|
|
5820
|
+
if (report.openTasks.length > 0) {
|
|
5821
|
+
lines.push(`- open tasks (${report.openTasks.length}):`);
|
|
5822
|
+
for (const t of report.openTasks) {
|
|
5823
|
+
lines.push(` - [ ] ${t.text} (${t.fromDate})`);
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
if (report.openDecisions.length > 0) {
|
|
5827
|
+
lines.push(`- open decisions (${report.openDecisions.length}):`);
|
|
5828
|
+
for (const d2 of report.openDecisions) {
|
|
5829
|
+
lines.push(` - ${d2.title} (${d2.date})`);
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5832
|
+
if (report.nothingOpen) {
|
|
5833
|
+
lines.push(`- nothing open in recent worklogs \u2014 you're clear. ${report.worklogCount} worklog(s), ${report.decisionCount} decision(s) on record.`);
|
|
5834
|
+
lines.push("- Leave a `## \uB2E4\uC74C \uC791\uC5C5` (or `## Next`) section in a worklog, or `- [ ] <task>` lines, to have them surface here next time.");
|
|
5835
|
+
}
|
|
5836
|
+
return lines.join("\n") + "\n";
|
|
3165
5837
|
}
|
|
3166
|
-
function renderUserProfile(name, role, task, date) {
|
|
3167
|
-
return `---
|
|
3168
|
-
name: user-profile
|
|
3169
|
-
description: Operator profile captured by /vortex init.
|
|
3170
|
-
type: user
|
|
3171
|
-
created: ${date}
|
|
3172
|
-
updated: ${date}
|
|
3173
|
-
---
|
|
3174
|
-
|
|
3175
|
-
# User Profile
|
|
3176
|
-
|
|
3177
|
-
- **Name/handle**: ${name}
|
|
3178
|
-
- **Role**: ${role}
|
|
3179
|
-
- **Initial focus**: ${task}
|
|
3180
5838
|
|
|
3181
|
-
|
|
5839
|
+
// ../plugins/session-rituals/dist/commands/agenda.js
|
|
5840
|
+
var agendaCommand = {
|
|
5841
|
+
name: "agenda",
|
|
5842
|
+
description: "What should I do today? Synthesize open tasks (- [ ] in recent worklogs), active decisions, and last activity from existing records.",
|
|
5843
|
+
handler: async (input) => {
|
|
5844
|
+
const opts = {};
|
|
5845
|
+
return collectAgenda(input.context, opts);
|
|
5846
|
+
}
|
|
5847
|
+
};
|
|
3182
5848
|
|
|
3183
|
-
|
|
3184
|
-
|
|
5849
|
+
// ../plugins/session-rituals/dist/registry.js
|
|
5850
|
+
function createRitualRegistry(options) {
|
|
5851
|
+
const registry = new CommandRegistry();
|
|
5852
|
+
registry.register(sessionStartCommand);
|
|
5853
|
+
registry.register(reindexCommand);
|
|
5854
|
+
registry.register(decisionCommand);
|
|
5855
|
+
registry.register(logCommand);
|
|
5856
|
+
registry.register(vortexCommand);
|
|
5857
|
+
registry.register(agendaCommand);
|
|
5858
|
+
if (options?.curate) {
|
|
5859
|
+
registry.register(curateCommand(options.curate));
|
|
5860
|
+
}
|
|
5861
|
+
if (options?.recall) {
|
|
5862
|
+
registry.register(recallCommand(options.recall));
|
|
5863
|
+
}
|
|
5864
|
+
return registry;
|
|
3185
5865
|
}
|
|
3186
|
-
function renderFirstTil(name, role, task, date) {
|
|
3187
|
-
return `---
|
|
3188
|
-
type: til
|
|
3189
|
-
status: active
|
|
3190
|
-
created: ${date}
|
|
3191
|
-
updated: ${date}
|
|
3192
|
-
tags: [til, onboarding]
|
|
3193
|
-
---
|
|
3194
|
-
|
|
3195
|
-
# ${date} \u2014 VortEX \uC2DC\uC791
|
|
3196
|
-
|
|
3197
|
-
> First TIL, created by \`/vortex init\`. ${name} (${role}). Today's focus: ${task}
|
|
3198
|
-
|
|
3199
|
-
## What I'm working on
|
|
3200
5866
|
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
5867
|
+
// ../plugins/session-rituals/dist/ambient-recall.js
|
|
5868
|
+
import { join as join23 } from "path";
|
|
5869
|
+
function defaultDbPath2(ctx) {
|
|
5870
|
+
return join23(ctx.dataDir, "_indexes", "memory.sqlite");
|
|
5871
|
+
}
|
|
5872
|
+
function createAmbientRecaller(ctx, options) {
|
|
5873
|
+
const resolveDb = options.dbPath ?? defaultDbPath2;
|
|
5874
|
+
const dbPath = resolveDb(ctx);
|
|
5875
|
+
return new AmbientRecaller({
|
|
5876
|
+
...options.minScore !== void 0 ? { minScore: options.minScore } : {},
|
|
5877
|
+
...options.maxSuggestions !== void 0 ? { maxSuggestions: options.maxSuggestions } : {},
|
|
5878
|
+
...options.minQueryChars !== void 0 ? { minQueryChars: options.minQueryChars } : {},
|
|
5879
|
+
recall: async (query, opts) => {
|
|
5880
|
+
const { sqlite, vector, recall: recallEngine } = await import("@vortex-os/memory-extended");
|
|
5881
|
+
const sqlStore = new sqlite.MemorySqliteStore(dbPath);
|
|
5882
|
+
const vecStore = new vector.MemoryVectorStore({ db: dbPath });
|
|
5883
|
+
const chunkStore = new vector.SessionChunkStore(dbPath);
|
|
5884
|
+
try {
|
|
5885
|
+
const result = await recallEngine.recall({
|
|
5886
|
+
query,
|
|
5887
|
+
...opts?.k !== void 0 ? { k: opts.k } : {},
|
|
5888
|
+
...options.source !== void 0 ? { source: options.source } : {}
|
|
5889
|
+
}, { sqlite: sqlStore, vector: vecStore, embed: options.embed, sessionChunks: chunkStore });
|
|
5890
|
+
return { hits: result.hits };
|
|
5891
|
+
} finally {
|
|
5892
|
+
chunkStore.close();
|
|
5893
|
+
vecStore.close();
|
|
5894
|
+
sqlStore.close();
|
|
5895
|
+
}
|
|
5896
|
+
}
|
|
5897
|
+
});
|
|
5898
|
+
}
|
|
3206
5899
|
|
|
3207
|
-
|
|
5900
|
+
// ../plugins/session-rituals/dist/session-start-report.js
|
|
5901
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5902
|
+
import { readdir as readdir16, readFile as readFile18 } from "fs/promises";
|
|
5903
|
+
import { join as join24 } from "path";
|
|
5904
|
+
var COUNTED_DIRS2 = ["_memory", "worklog", "decision-log"];
|
|
5905
|
+
var DEFAULT_GAP_WINDOW_DAYS = 30;
|
|
5906
|
+
var BOOT_BANNER = String.raw`
|
|
5907
|
+
__ __ _ _____ __
|
|
5908
|
+
\ \ / ___ _ _| |_| __\ \/ /
|
|
5909
|
+
\ V / _ | '_| _| _| > <
|
|
5910
|
+
\_/\___|_| \__|___/_/\_\
|
|
5911
|
+
Vortex absorbs context · EX executes it`;
|
|
5912
|
+
async function collectSessionStartReport(ctx, opts) {
|
|
5913
|
+
const now = opts?.now ?? /* @__PURE__ */ new Date();
|
|
5914
|
+
const counts = {};
|
|
5915
|
+
const missing = [];
|
|
5916
|
+
for (const name of COUNTED_DIRS2) {
|
|
5917
|
+
const dir = join24(ctx.dataDir, name);
|
|
5918
|
+
if (!existsSync9(dir)) {
|
|
5919
|
+
missing.push(name);
|
|
5920
|
+
counts[name] = 0;
|
|
5921
|
+
continue;
|
|
5922
|
+
}
|
|
5923
|
+
counts[name] = await countMarkdown3(dir, name === "worklog");
|
|
5924
|
+
}
|
|
5925
|
+
const { recent, dates } = await scanWorklog(ctx.dataDir);
|
|
5926
|
+
const cutoff = isoDate(addDays(now, -(opts?.gapWindowDays ?? DEFAULT_GAP_WINDOW_DAYS)));
|
|
5927
|
+
const recentWorklogDates = dates.filter((d2) => d2 >= cutoff);
|
|
5928
|
+
return {
|
|
5929
|
+
time: now.toISOString(),
|
|
5930
|
+
repoRoot: ctx.repoRoot,
|
|
5931
|
+
dataDir: ctx.dataDir,
|
|
5932
|
+
counts,
|
|
5933
|
+
missing,
|
|
5934
|
+
recentWorklog: recent,
|
|
5935
|
+
recentWorklogDates,
|
|
5936
|
+
environment: opts?.environment ?? null
|
|
5937
|
+
};
|
|
5938
|
+
}
|
|
5939
|
+
function detectWorklogGaps(commitDays, presentDates) {
|
|
5940
|
+
const present = new Set(presentDates);
|
|
5941
|
+
return [...new Set(commitDays)].filter((d2) => d2 && !present.has(d2)).sort();
|
|
5942
|
+
}
|
|
5943
|
+
function renderSessionStartReport(report, extras) {
|
|
5944
|
+
const lines = [BOOT_BANNER, ""];
|
|
5945
|
+
const env = report.environment ? ` \xB7 env: ${report.environment}` : "";
|
|
5946
|
+
lines.push(`- time: ${report.time}${env}`);
|
|
5947
|
+
const git = extras?.git;
|
|
5948
|
+
if (git?.ran) {
|
|
5949
|
+
lines.push(git.conflict ? `- git: \u26A0\uFE0F ${git.summary} \u2014 resolve manually (not auto-resolved)` : `- git: ${git.summary}`);
|
|
5950
|
+
}
|
|
5951
|
+
const countStr = COUNTED_DIRS2.map((d2) => `${d2} ${report.counts[d2] ?? 0}`).join(" \xB7 ");
|
|
5952
|
+
const miss = report.missing.length ? ` (missing: ${report.missing.join(", ")})` : "";
|
|
5953
|
+
lines.push(`- data: ${countStr}${miss}`);
|
|
5954
|
+
lines.push(report.recentWorklog ? `- last worklog: ${report.recentWorklog.title} (${report.recentWorklog.path})` : `- last worklog: none yet`);
|
|
5955
|
+
const gaps = extras?.missingWorklogDays ?? [];
|
|
5956
|
+
if (gaps.length) {
|
|
5957
|
+
lines.push(`- \u26A0\uFE0F work without a worklog: ${gaps.join(", ")} \u2014 backfill from that day's commits`);
|
|
5958
|
+
}
|
|
5959
|
+
const cu = extras?.catchUp;
|
|
5960
|
+
if (cu && (cu.ingestedLocal > 0 || cu.indexedPulled > 0 || cu.errors > 0)) {
|
|
5961
|
+
const parts = [];
|
|
5962
|
+
if (cu.ingestedLocal > 0)
|
|
5963
|
+
parts.push(`${cu.ingestedLocal} new`);
|
|
5964
|
+
if (cu.indexedPulled > 0)
|
|
5965
|
+
parts.push(`${cu.indexedPulled} pulled`);
|
|
5966
|
+
const n = cu.ingestedLocal + cu.indexedPulled;
|
|
5967
|
+
let line = `- caught up: archived ${parts.join(" + ")} conversation${n === 1 ? "" : "s"}`;
|
|
5968
|
+
if (cu.errors > 0)
|
|
5969
|
+
line += ` (${cu.errors} error${cu.errors === 1 ? "" : "s"})`;
|
|
5970
|
+
lines.push(line);
|
|
5971
|
+
}
|
|
5972
|
+
return lines.join("\n") + "\n";
|
|
5973
|
+
}
|
|
5974
|
+
async function countMarkdown3(dir, recursive) {
|
|
5975
|
+
let total = 0;
|
|
5976
|
+
const entries = await readdir16(dir, { withFileTypes: true });
|
|
5977
|
+
for (const e of entries) {
|
|
5978
|
+
if (e.isFile()) {
|
|
5979
|
+
if (!e.name.endsWith(".md"))
|
|
5980
|
+
continue;
|
|
5981
|
+
if (e.name === "README.md" || e.name === "_INDEX.md" || e.name === "MEMORY.md")
|
|
5982
|
+
continue;
|
|
5983
|
+
if (e.name.startsWith("_TEMPLATE"))
|
|
5984
|
+
continue;
|
|
5985
|
+
total++;
|
|
5986
|
+
} else if (e.isDirectory() && recursive) {
|
|
5987
|
+
if (e.name.startsWith(".") || e.name.startsWith("_"))
|
|
5988
|
+
continue;
|
|
5989
|
+
total += await countMarkdown3(join24(dir, e.name), recursive);
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
return total;
|
|
5993
|
+
}
|
|
5994
|
+
async function scanWorklog(dataDir) {
|
|
5995
|
+
const root = join24(dataDir, "worklog");
|
|
5996
|
+
if (!existsSync9(root))
|
|
5997
|
+
return { recent: null, dates: [] };
|
|
5998
|
+
let bestRel = null;
|
|
5999
|
+
const dates = /* @__PURE__ */ new Set();
|
|
6000
|
+
async function walk5(absDir, rel) {
|
|
6001
|
+
let entries;
|
|
6002
|
+
try {
|
|
6003
|
+
entries = await readdir16(absDir, { withFileTypes: true });
|
|
6004
|
+
} catch {
|
|
6005
|
+
return;
|
|
6006
|
+
}
|
|
6007
|
+
for (const e of entries) {
|
|
6008
|
+
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
6009
|
+
if (e.isDirectory()) {
|
|
6010
|
+
await walk5(join24(absDir, e.name), childRel);
|
|
6011
|
+
} else if (e.isFile()) {
|
|
6012
|
+
const m2 = e.name.match(/^(\d{4}-\d{2}-\d{2})-.+\.md$/);
|
|
6013
|
+
if (!m2)
|
|
6014
|
+
continue;
|
|
6015
|
+
dates.add(m2[1]);
|
|
6016
|
+
if (bestRel === null || childRel > bestRel)
|
|
6017
|
+
bestRel = childRel;
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
await walk5(root, "");
|
|
6022
|
+
const recent = bestRel === null ? null : { path: `worklog/${bestRel}`, title: await readTitle(join24(root, bestRel)) };
|
|
6023
|
+
return { recent, dates: [...dates] };
|
|
6024
|
+
}
|
|
6025
|
+
async function readTitle(absPath) {
|
|
6026
|
+
try {
|
|
6027
|
+
const raw = await readFile18(absPath, "utf8");
|
|
6028
|
+
const m2 = raw.match(/^#\s+(.+)$/m);
|
|
6029
|
+
if (m2)
|
|
6030
|
+
return m2[1].trim();
|
|
6031
|
+
} catch {
|
|
6032
|
+
}
|
|
6033
|
+
const base = absPath.replace(/\\/g, "/").split("/").pop() ?? absPath;
|
|
6034
|
+
return base.replace(/\.md$/, "");
|
|
6035
|
+
}
|
|
6036
|
+
function addDays(d2, n) {
|
|
6037
|
+
const out = new Date(d2);
|
|
6038
|
+
out.setDate(out.getDate() + n);
|
|
6039
|
+
return out;
|
|
6040
|
+
}
|
|
6041
|
+
function isoDate(d2) {
|
|
6042
|
+
const y2 = d2.getFullYear();
|
|
6043
|
+
const m2 = String(d2.getMonth() + 1).padStart(2, "0");
|
|
6044
|
+
const day = String(d2.getDate()).padStart(2, "0");
|
|
6045
|
+
return `${y2}-${m2}-${day}`;
|
|
6046
|
+
}
|
|
3208
6047
|
|
|
3209
|
-
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
6048
|
+
// ../plugins/session-rituals/dist/worklog-write.js
|
|
6049
|
+
import { mkdir as mkdir6, writeFile as writeFile11 } from "fs/promises";
|
|
6050
|
+
import { dirname as dirname5, join as join25 } from "path";
|
|
6051
|
+
async function ensureWorklogEntry(ctx, opts) {
|
|
6052
|
+
const date = isoDate2(opts?.now ?? /* @__PURE__ */ new Date());
|
|
6053
|
+
const keyword = (opts?.keyword ?? "worklog").trim() || "worklog";
|
|
6054
|
+
const store = new WorklogStore(join25(ctx.dataDir, "worklog"));
|
|
6055
|
+
const existing = await store.get(date);
|
|
6056
|
+
if (existing) {
|
|
6057
|
+
return { path: existing.path, date: existing.date, keyword: existing.keyword, created: false };
|
|
6058
|
+
}
|
|
6059
|
+
const path = store.pathFor(date, keyword);
|
|
6060
|
+
const title = opts?.title ?? `${date} worklog`;
|
|
6061
|
+
await mkdir6(dirname5(path), { recursive: true });
|
|
6062
|
+
await writeFile11(path, renderWorklogFile(date, title, opts?.body ?? ""), "utf8");
|
|
6063
|
+
return { path, date, keyword, created: true };
|
|
3213
6064
|
}
|
|
3214
|
-
function
|
|
6065
|
+
function renderWorklogFile(date, title, body) {
|
|
6066
|
+
const trimmed = body.trimEnd();
|
|
3215
6067
|
return `---
|
|
3216
|
-
type:
|
|
3217
|
-
status: active
|
|
3218
|
-
privacy: internal
|
|
6068
|
+
type: worklog
|
|
3219
6069
|
created: ${date}
|
|
3220
6070
|
updated: ${date}
|
|
3221
|
-
tags: [
|
|
6071
|
+
tags: [worklog]
|
|
3222
6072
|
---
|
|
3223
6073
|
|
|
3224
|
-
# ${
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
## Active TILs
|
|
3229
|
-
|
|
3230
|
-
- (link your ${role}-related TILs here as you accumulate them)
|
|
3231
|
-
|
|
3232
|
-
## Decisions
|
|
3233
|
-
|
|
3234
|
-
- (link Decision Log entries here)
|
|
3235
|
-
|
|
3236
|
-
## Memories
|
|
3237
|
-
|
|
3238
|
-
- (link \`_memory/*.md\` entries here)
|
|
3239
|
-
|
|
3240
|
-
## External references
|
|
3241
|
-
|
|
3242
|
-
- (links, dashboards, repos, etc.)
|
|
3243
|
-
`;
|
|
3244
|
-
}
|
|
3245
|
-
function slugify(s) {
|
|
3246
|
-
return s.toLowerCase().replace(/[^a-z0-9가-힣]+/g, "-").replace(/^-+|-+$/g, "");
|
|
6074
|
+
# ${title}
|
|
6075
|
+
` + (trimmed ? `
|
|
6076
|
+
${trimmed}
|
|
6077
|
+
` : ``);
|
|
3247
6078
|
}
|
|
3248
|
-
function
|
|
3249
|
-
const d2 = /* @__PURE__ */ new Date();
|
|
6079
|
+
function isoDate2(d2) {
|
|
3250
6080
|
const y2 = d2.getFullYear();
|
|
3251
6081
|
const m2 = String(d2.getMonth() + 1).padStart(2, "0");
|
|
3252
6082
|
const day = String(d2.getDate()).padStart(2, "0");
|
|
3253
6083
|
return `${y2}-${m2}-${day}`;
|
|
3254
6084
|
}
|
|
3255
6085
|
|
|
3256
|
-
// ../plugins/session-rituals/dist/
|
|
3257
|
-
function
|
|
3258
|
-
const
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
6086
|
+
// ../plugins/session-rituals/dist/catch-up.js
|
|
6087
|
+
async function catchUpSessions(ctx, opts) {
|
|
6088
|
+
const { sessionArchive } = await import("@vortex-os/memory-extended");
|
|
6089
|
+
const dataDir = ctx.dataDir;
|
|
6090
|
+
const cwd = opts?.cwd ?? ctx.repoRoot;
|
|
6091
|
+
const adapters = opts?.adapters ?? [sessionArchive.claudeCodeAdapter];
|
|
6092
|
+
const ingestResult = await sessionArchive.ingest({ adapters, dataDir, cwd, env: opts?.env });
|
|
6093
|
+
const store = new sessionArchive.SessionArchiveStore(dataDir);
|
|
6094
|
+
let indexedPulled = 0;
|
|
6095
|
+
try {
|
|
6096
|
+
indexedPulled = store.reindexFromNormalized().indexed;
|
|
6097
|
+
} finally {
|
|
6098
|
+
store.close();
|
|
6099
|
+
}
|
|
6100
|
+
return {
|
|
6101
|
+
ingestedLocal: ingestResult.sessionsIngested,
|
|
6102
|
+
indexedPulled,
|
|
6103
|
+
errors: ingestResult.errors.length
|
|
6104
|
+
};
|
|
3265
6105
|
}
|
|
3266
6106
|
export {
|
|
3267
6107
|
dist_exports5 as aiCodingPitfalls,
|
|
@@ -3271,11 +6111,12 @@ export {
|
|
|
3271
6111
|
dist_exports10 as indexGenerator,
|
|
3272
6112
|
dist_exports12 as linkRewriter,
|
|
3273
6113
|
dist_exports3 as memorySystem,
|
|
6114
|
+
dist_exports13 as proactiveCurator,
|
|
3274
6115
|
dist_exports7 as reportGenerator,
|
|
3275
6116
|
dist_exports11 as runbooks,
|
|
3276
|
-
|
|
6117
|
+
dist_exports14 as sessionRituals,
|
|
3277
6118
|
dist_exports2 as slashCommands,
|
|
3278
|
-
|
|
3279
|
-
|
|
6119
|
+
dist_exports6 as toolRules,
|
|
6120
|
+
dist_exports8 as worklog
|
|
3280
6121
|
};
|
|
3281
6122
|
//# sourceMappingURL=index.js.map
|