@locusai/cli 0.25.6 → 0.26.0
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/bin/locus.js +1128 -362
- package/package.json +2 -2
package/bin/locus.js
CHANGED
|
@@ -1880,6 +1880,210 @@ var init_github = __esm(() => {
|
|
|
1880
1880
|
init_rate_limiter();
|
|
1881
1881
|
});
|
|
1882
1882
|
|
|
1883
|
+
// src/core/memory.ts
|
|
1884
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
|
|
1885
|
+
import {
|
|
1886
|
+
appendFile,
|
|
1887
|
+
mkdir,
|
|
1888
|
+
readFile,
|
|
1889
|
+
stat,
|
|
1890
|
+
unlink,
|
|
1891
|
+
writeFile
|
|
1892
|
+
} from "node:fs/promises";
|
|
1893
|
+
import { join as join6 } from "node:path";
|
|
1894
|
+
function getMemoryDir(projectRoot) {
|
|
1895
|
+
return join6(projectRoot, ".locus", MEMORY_DIR);
|
|
1896
|
+
}
|
|
1897
|
+
async function ensureMemoryDir(projectRoot) {
|
|
1898
|
+
const dir = getMemoryDir(projectRoot);
|
|
1899
|
+
await mkdir(dir, { recursive: true });
|
|
1900
|
+
for (const category of Object.values(MEMORY_CATEGORIES)) {
|
|
1901
|
+
const filePath = join6(dir, category.file);
|
|
1902
|
+
if (!existsSync6(filePath)) {
|
|
1903
|
+
const header = `# ${category.title}
|
|
1904
|
+
|
|
1905
|
+
${category.description}
|
|
1906
|
+
|
|
1907
|
+
`;
|
|
1908
|
+
await writeFile(filePath, header, "utf-8");
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
async function readMemoryFile(projectRoot, category) {
|
|
1913
|
+
const meta = MEMORY_CATEGORIES[category];
|
|
1914
|
+
if (!meta)
|
|
1915
|
+
return "";
|
|
1916
|
+
const filePath = join6(getMemoryDir(projectRoot), meta.file);
|
|
1917
|
+
try {
|
|
1918
|
+
return await readFile(filePath, "utf-8");
|
|
1919
|
+
} catch {
|
|
1920
|
+
return "";
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
async function readAllMemory(projectRoot) {
|
|
1924
|
+
const parts = [];
|
|
1925
|
+
for (const category of Object.keys(MEMORY_CATEGORIES)) {
|
|
1926
|
+
const content = await readMemoryFile(projectRoot, category);
|
|
1927
|
+
if (content.trim()) {
|
|
1928
|
+
parts.push(content.trim());
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return parts.join(`
|
|
1932
|
+
|
|
1933
|
+
`);
|
|
1934
|
+
}
|
|
1935
|
+
function readAllMemorySync(projectRoot) {
|
|
1936
|
+
const dir = getMemoryDir(projectRoot);
|
|
1937
|
+
if (!existsSync6(dir))
|
|
1938
|
+
return "";
|
|
1939
|
+
const parts = [];
|
|
1940
|
+
for (const meta of Object.values(MEMORY_CATEGORIES)) {
|
|
1941
|
+
const filePath = join6(dir, meta.file);
|
|
1942
|
+
try {
|
|
1943
|
+
const content = readFileSync4(filePath, "utf-8").trim();
|
|
1944
|
+
if (content)
|
|
1945
|
+
parts.push(content);
|
|
1946
|
+
} catch {}
|
|
1947
|
+
}
|
|
1948
|
+
return parts.join(`
|
|
1949
|
+
|
|
1950
|
+
`);
|
|
1951
|
+
}
|
|
1952
|
+
async function appendMemoryEntries(projectRoot, entries) {
|
|
1953
|
+
const dir = getMemoryDir(projectRoot);
|
|
1954
|
+
for (const entry of entries) {
|
|
1955
|
+
const meta = MEMORY_CATEGORIES[entry.category];
|
|
1956
|
+
if (!meta)
|
|
1957
|
+
continue;
|
|
1958
|
+
const filePath = join6(dir, meta.file);
|
|
1959
|
+
const line = `- **[${meta.title}]**: ${entry.text}
|
|
1960
|
+
`;
|
|
1961
|
+
await appendFile(filePath, line, "utf-8");
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
function resolveLearningsCategory(tag) {
|
|
1965
|
+
return LEARNINGS_CATEGORY_MAP[tag.toLowerCase()] ?? "conventions";
|
|
1966
|
+
}
|
|
1967
|
+
async function migrateFromLearnings(projectRoot) {
|
|
1968
|
+
const learningsPath = join6(projectRoot, ".locus", "LEARNINGS.md");
|
|
1969
|
+
let content;
|
|
1970
|
+
try {
|
|
1971
|
+
content = await readFile(learningsPath, "utf-8");
|
|
1972
|
+
} catch {
|
|
1973
|
+
return { migrated: 0, skipped: 0 };
|
|
1974
|
+
}
|
|
1975
|
+
if (!content.trim()) {
|
|
1976
|
+
return { migrated: 0, skipped: 0 };
|
|
1977
|
+
}
|
|
1978
|
+
const entryPattern = /^- \*\*\[([^\]]+)\]\*\*:\s*/;
|
|
1979
|
+
const lines = content.split(`
|
|
1980
|
+
`);
|
|
1981
|
+
const parsed = [];
|
|
1982
|
+
for (let i = 0;i < lines.length; i++) {
|
|
1983
|
+
const match = lines[i].match(entryPattern);
|
|
1984
|
+
if (!match)
|
|
1985
|
+
continue;
|
|
1986
|
+
const tag = match[1];
|
|
1987
|
+
const category = resolveLearningsCategory(tag);
|
|
1988
|
+
let text = lines[i].slice(match[0].length);
|
|
1989
|
+
for (let j = i + 1;j < lines.length; j++) {
|
|
1990
|
+
if (lines[j].match(entryPattern) || lines[j].trim() === "")
|
|
1991
|
+
break;
|
|
1992
|
+
text += `
|
|
1993
|
+
${lines[j]}`;
|
|
1994
|
+
i = j;
|
|
1995
|
+
}
|
|
1996
|
+
parsed.push({ category, text: text.trim() });
|
|
1997
|
+
}
|
|
1998
|
+
if (parsed.length === 0) {
|
|
1999
|
+
return { migrated: 0, skipped: 0 };
|
|
2000
|
+
}
|
|
2001
|
+
await ensureMemoryDir(projectRoot);
|
|
2002
|
+
const existingContent = {};
|
|
2003
|
+
for (const key of Object.keys(MEMORY_CATEGORIES)) {
|
|
2004
|
+
existingContent[key] = await readMemoryFile(projectRoot, key);
|
|
2005
|
+
}
|
|
2006
|
+
let migrated = 0;
|
|
2007
|
+
let skipped = 0;
|
|
2008
|
+
for (const entry of parsed) {
|
|
2009
|
+
const existing = existingContent[entry.category] ?? "";
|
|
2010
|
+
if (existing.includes(entry.text)) {
|
|
2011
|
+
skipped++;
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
await appendMemoryEntries(projectRoot, [entry]);
|
|
2015
|
+
existingContent[entry.category] = (existingContent[entry.category] ?? "") + `- **[${MEMORY_CATEGORIES[entry.category]?.title}]**: ${entry.text}
|
|
2016
|
+
`;
|
|
2017
|
+
migrated++;
|
|
2018
|
+
}
|
|
2019
|
+
if (migrated > 0 || skipped > 0) {
|
|
2020
|
+
try {
|
|
2021
|
+
await unlink(learningsPath);
|
|
2022
|
+
} catch {}
|
|
2023
|
+
}
|
|
2024
|
+
return { migrated, skipped };
|
|
2025
|
+
}
|
|
2026
|
+
async function getMemoryStats(projectRoot) {
|
|
2027
|
+
const dir = getMemoryDir(projectRoot);
|
|
2028
|
+
const result = {};
|
|
2029
|
+
for (const [key, meta] of Object.entries(MEMORY_CATEGORIES)) {
|
|
2030
|
+
const filePath = join6(dir, meta.file);
|
|
2031
|
+
try {
|
|
2032
|
+
const [content, fileStat] = await Promise.all([
|
|
2033
|
+
readFile(filePath, "utf-8"),
|
|
2034
|
+
stat(filePath)
|
|
2035
|
+
]);
|
|
2036
|
+
const count = content.split(`
|
|
2037
|
+
`).filter((line) => line.startsWith("- ")).length;
|
|
2038
|
+
result[key] = {
|
|
2039
|
+
count,
|
|
2040
|
+
size: fileStat.size,
|
|
2041
|
+
lastModified: fileStat.mtime
|
|
2042
|
+
};
|
|
2043
|
+
} catch {
|
|
2044
|
+
result[key] = { count: 0, size: 0, lastModified: new Date(0) };
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
return result;
|
|
2048
|
+
}
|
|
2049
|
+
var MEMORY_DIR = "memory", MEMORY_CATEGORIES, LEARNINGS_CATEGORY_MAP;
|
|
2050
|
+
var init_memory = __esm(() => {
|
|
2051
|
+
MEMORY_CATEGORIES = {
|
|
2052
|
+
architecture: {
|
|
2053
|
+
file: "architecture.md",
|
|
2054
|
+
title: "Architecture",
|
|
2055
|
+
description: "Package ownership, module boundaries, data flow"
|
|
2056
|
+
},
|
|
2057
|
+
conventions: {
|
|
2058
|
+
file: "conventions.md",
|
|
2059
|
+
title: "Conventions",
|
|
2060
|
+
description: "Code style, naming, patterns"
|
|
2061
|
+
},
|
|
2062
|
+
decisions: {
|
|
2063
|
+
file: "decisions.md",
|
|
2064
|
+
title: "Decisions",
|
|
2065
|
+
description: "Trade-off rationale: why X over Y"
|
|
2066
|
+
},
|
|
2067
|
+
preferences: {
|
|
2068
|
+
file: "preferences.md",
|
|
2069
|
+
title: "Preferences",
|
|
2070
|
+
description: "User corrections, rejected approaches"
|
|
2071
|
+
},
|
|
2072
|
+
debugging: {
|
|
2073
|
+
file: "debugging.md",
|
|
2074
|
+
title: "Debugging",
|
|
2075
|
+
description: "Non-obvious gotchas, environment quirks"
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
LEARNINGS_CATEGORY_MAP = {
|
|
2079
|
+
architecture: "architecture",
|
|
2080
|
+
conventions: "conventions",
|
|
2081
|
+
packages: "architecture",
|
|
2082
|
+
"user preferences": "preferences",
|
|
2083
|
+
debugging: "debugging"
|
|
2084
|
+
};
|
|
2085
|
+
});
|
|
2086
|
+
|
|
1883
2087
|
// src/types.ts
|
|
1884
2088
|
var PRIORITY_LABELS, TYPE_LABELS, STATUS_LABELS, AGENT_LABEL, ALL_LABELS;
|
|
1885
2089
|
var init_types = __esm(() => {
|
|
@@ -1937,8 +2141,8 @@ var exports_init = {};
|
|
|
1937
2141
|
__export(exports_init, {
|
|
1938
2142
|
initCommand: () => initCommand
|
|
1939
2143
|
});
|
|
1940
|
-
import { existsSync as
|
|
1941
|
-
import { join as
|
|
2144
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
2145
|
+
import { join as join7 } from "node:path";
|
|
1942
2146
|
async function initCommand(cwd) {
|
|
1943
2147
|
const log = getLogger();
|
|
1944
2148
|
process.stderr.write(`
|
|
@@ -1982,23 +2186,47 @@ ${bold2("Initializing Locus...")}
|
|
|
1982
2186
|
}
|
|
1983
2187
|
process.stderr.write(`${green("✓")} Repository: ${bold2(`${context.owner}/${context.repo}`)} (branch: ${context.defaultBranch})
|
|
1984
2188
|
`);
|
|
1985
|
-
const locusDir =
|
|
2189
|
+
const locusDir = join7(cwd, ".locus");
|
|
1986
2190
|
const dirs = [
|
|
1987
2191
|
locusDir,
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2192
|
+
join7(locusDir, "sessions"),
|
|
2193
|
+
join7(locusDir, "discussions"),
|
|
2194
|
+
join7(locusDir, "artifacts"),
|
|
2195
|
+
join7(locusDir, "plans"),
|
|
2196
|
+
join7(locusDir, "logs"),
|
|
2197
|
+
join7(locusDir, "run-state")
|
|
1994
2198
|
];
|
|
1995
2199
|
for (const dir of dirs) {
|
|
1996
|
-
if (!
|
|
2200
|
+
if (!existsSync7(dir)) {
|
|
1997
2201
|
mkdirSync5(dir, { recursive: true });
|
|
1998
2202
|
}
|
|
1999
2203
|
}
|
|
2000
2204
|
process.stderr.write(`${green("✓")} Created .locus/ directory structure
|
|
2001
2205
|
`);
|
|
2206
|
+
const memoryDirExists = existsSync7(getMemoryDir(cwd));
|
|
2207
|
+
await ensureMemoryDir(cwd);
|
|
2208
|
+
if (!memoryDirExists) {
|
|
2209
|
+
process.stderr.write(`${green("✓")} Created .locus/memory/ with category files
|
|
2210
|
+
`);
|
|
2211
|
+
} else {
|
|
2212
|
+
process.stderr.write(`${dim2("○")} .locus/memory/ already exists (preserved)
|
|
2213
|
+
`);
|
|
2214
|
+
}
|
|
2215
|
+
if (existsSync7(join7(locusDir, "LEARNINGS.md"))) {
|
|
2216
|
+
const result = await migrateFromLearnings(cwd);
|
|
2217
|
+
if (result.migrated > 0) {
|
|
2218
|
+
process.stderr.write(`${green("✓")} Migrated ${result.migrated} entries from LEARNINGS.md to .locus/memory/
|
|
2219
|
+
`);
|
|
2220
|
+
}
|
|
2221
|
+
if (result.skipped > 0) {
|
|
2222
|
+
process.stderr.write(`${dim2("○")} Skipped ${result.skipped} duplicate entries during migration
|
|
2223
|
+
`);
|
|
2224
|
+
}
|
|
2225
|
+
if (result.migrated > 0 || result.skipped > 0) {
|
|
2226
|
+
process.stderr.write(`${green("✓")} Removed legacy LEARNINGS.md
|
|
2227
|
+
`);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2002
2230
|
const isReInit = isInitialized(cwd);
|
|
2003
2231
|
const config = {
|
|
2004
2232
|
...DEFAULT_CONFIG2,
|
|
@@ -2014,7 +2242,7 @@ ${bold2("Initializing Locus...")}
|
|
|
2014
2242
|
};
|
|
2015
2243
|
if (isReInit) {
|
|
2016
2244
|
try {
|
|
2017
|
-
const existing = JSON.parse(
|
|
2245
|
+
const existing = JSON.parse(readFileSync5(join7(locusDir, "config.json"), "utf-8"));
|
|
2018
2246
|
if (existing.ai)
|
|
2019
2247
|
config.ai = { ...config.ai, ...existing.ai };
|
|
2020
2248
|
if (existing.agent)
|
|
@@ -2035,8 +2263,8 @@ ${bold2("Initializing Locus...")}
|
|
|
2035
2263
|
`);
|
|
2036
2264
|
}
|
|
2037
2265
|
saveConfig(cwd, config);
|
|
2038
|
-
const locusMdPath =
|
|
2039
|
-
if (!
|
|
2266
|
+
const locusMdPath = join7(locusDir, "LOCUS.md");
|
|
2267
|
+
if (!existsSync7(locusMdPath)) {
|
|
2040
2268
|
writeFileSync4(locusMdPath, LOCUS_MD_TEMPLATE, "utf-8");
|
|
2041
2269
|
process.stderr.write(`${green("✓")} Generated LOCUS.md (edit to add project context)
|
|
2042
2270
|
`);
|
|
@@ -2044,17 +2272,8 @@ ${bold2("Initializing Locus...")}
|
|
|
2044
2272
|
process.stderr.write(`${dim2("○")} LOCUS.md already exists (preserved)
|
|
2045
2273
|
`);
|
|
2046
2274
|
}
|
|
2047
|
-
const
|
|
2048
|
-
if (!
|
|
2049
|
-
writeFileSync4(learningsMdPath, LEARNINGS_MD_TEMPLATE, "utf-8");
|
|
2050
|
-
process.stderr.write(`${green("✓")} Generated LEARNINGS.md
|
|
2051
|
-
`);
|
|
2052
|
-
} else {
|
|
2053
|
-
process.stderr.write(`${dim2("○")} LEARNINGS.md already exists (preserved)
|
|
2054
|
-
`);
|
|
2055
|
-
}
|
|
2056
|
-
const sandboxIgnorePath = join6(cwd, ".sandboxignore");
|
|
2057
|
-
if (!existsSync6(sandboxIgnorePath)) {
|
|
2275
|
+
const sandboxIgnorePath = join7(cwd, ".sandboxignore");
|
|
2276
|
+
if (!existsSync7(sandboxIgnorePath)) {
|
|
2058
2277
|
writeFileSync4(sandboxIgnorePath, SANDBOXIGNORE_TEMPLATE, "utf-8");
|
|
2059
2278
|
process.stderr.write(`${green("✓")} Generated .sandboxignore
|
|
2060
2279
|
`);
|
|
@@ -2063,8 +2282,8 @@ ${bold2("Initializing Locus...")}
|
|
|
2063
2282
|
`);
|
|
2064
2283
|
}
|
|
2065
2284
|
const ecosystem = detectProjectEcosystem(cwd);
|
|
2066
|
-
const sandboxSetupPath =
|
|
2067
|
-
if (!
|
|
2285
|
+
const sandboxSetupPath = join7(locusDir, "sandbox-setup.sh");
|
|
2286
|
+
if (!existsSync7(sandboxSetupPath)) {
|
|
2068
2287
|
const template = generateSandboxSetupTemplate(ecosystem);
|
|
2069
2288
|
if (template) {
|
|
2070
2289
|
writeFileSync4(sandboxSetupPath, template, {
|
|
@@ -2087,10 +2306,10 @@ ${bold2("Initializing Locus...")}
|
|
|
2087
2306
|
process.stderr.write(`\r${yellow2("⚠")} Some labels could not be created: ${e.message}
|
|
2088
2307
|
`);
|
|
2089
2308
|
}
|
|
2090
|
-
const gitignorePath =
|
|
2309
|
+
const gitignorePath = join7(cwd, ".gitignore");
|
|
2091
2310
|
let gitignoreContent = "";
|
|
2092
|
-
if (
|
|
2093
|
-
gitignoreContent =
|
|
2311
|
+
if (existsSync7(gitignorePath)) {
|
|
2312
|
+
gitignoreContent = readFileSync5(gitignorePath, "utf-8");
|
|
2094
2313
|
}
|
|
2095
2314
|
const entriesToAdd = GITIGNORE_ENTRIES.filter((entry) => entry && !gitignoreContent.includes(entry.trim()));
|
|
2096
2315
|
if (entriesToAdd.length > 0) {
|
|
@@ -2222,12 +2441,19 @@ When a task produces knowledge, analysis, or research output rather than (or in
|
|
|
2222
2441
|
|
|
2223
2442
|
## Continuous Learning (MANDATORY)
|
|
2224
2443
|
|
|
2225
|
-
**CRITICAL: Updating \`.locus/
|
|
2444
|
+
**CRITICAL: Updating \`.locus/memory/\` is a required step, not optional.** You MUST read memory files before starting work AND update them before finishing if you learned anything worth recording. Failing to update memory when a reusable lesson was discovered is a defect — treat it with the same severity as forgetting to run tests.
|
|
2445
|
+
|
|
2446
|
+
**Memory is organized into 5 category files in \`.locus/memory/\`:**
|
|
2447
|
+
- \`architecture.md\` — Package ownership, module boundaries, data flow
|
|
2448
|
+
- \`conventions.md\` — Code style, naming, patterns
|
|
2449
|
+
- \`decisions.md\` — Trade-off rationale: why X over Y
|
|
2450
|
+
- \`preferences.md\` — User corrections, rejected approaches
|
|
2451
|
+
- \`debugging.md\` — Non-obvious gotchas, environment quirks
|
|
2226
2452
|
|
|
2227
2453
|
**Workflow:**
|
|
2228
|
-
1. **Read** \`.locus/
|
|
2454
|
+
1. **Read** files in \`.locus/memory/\` at the start of every task
|
|
2229
2455
|
2. **During execution**, note any reusable lessons (architectural discoveries, user corrections, non-obvious constraints)
|
|
2230
|
-
3. **Before finishing**, append new entries to
|
|
2456
|
+
3. **Before finishing**, append new entries to the appropriate category file in \`.locus/memory/\`. Do this as one of your final steps, alongside running tests and linters
|
|
2231
2457
|
|
|
2232
2458
|
**The quality bar:** Ask yourself — "Would a new agent working on a completely different task benefit from knowing this?" If yes, record it. If it only matters for the current task or file, skip it.
|
|
2233
2459
|
|
|
@@ -2253,19 +2479,19 @@ When a task produces knowledge, analysis, or research output rather than (or in
|
|
|
2253
2479
|
**Good examples:**
|
|
2254
2480
|
- \`[Architecture]\`: Shared types for all packages live in \`@locusai/shared\` — never redefine them locally in CLI or API packages.
|
|
2255
2481
|
- \`[User Preferences]\`: User prefers not to track low-level interrupt/signal handling patterns in learnings — focus on architectural and decision-level entries.
|
|
2256
|
-
- \`[
|
|
2482
|
+
- \`[Conventions]\`: Validation uses Zod throughout — do not introduce a second validation library.
|
|
2257
2483
|
|
|
2258
2484
|
**Bad examples (do not write these):**
|
|
2259
|
-
- \`[
|
|
2485
|
+
- \`[Conventions]\`: \`run.ts\` must call \`registerShutdownHandlers()\` at startup. ← too local, obvious from the file.
|
|
2260
2486
|
- \`[Debugging]\`: Fixed a regex bug in \`image-detect.ts\`. ← one-time fix, irrelevant to future tasks.
|
|
2261
2487
|
|
|
2262
|
-
**Format (append
|
|
2488
|
+
**Format (append to the appropriate category file):**
|
|
2263
2489
|
|
|
2264
2490
|
\`\`\`
|
|
2265
2491
|
- **[Category]**: Concise description (1-2 lines max). *Rationale if non-obvious.*
|
|
2266
2492
|
\`\`\`
|
|
2267
2493
|
|
|
2268
|
-
**Categories:** Architecture,
|
|
2494
|
+
**Categories:** Architecture, Conventions, Decisions, Preferences, Debugging
|
|
2269
2495
|
|
|
2270
2496
|
## Error Handling
|
|
2271
2497
|
|
|
@@ -2317,12 +2543,6 @@ service-account*.json
|
|
|
2317
2543
|
|
|
2318
2544
|
# Docker secrets
|
|
2319
2545
|
docker-compose.override.yml
|
|
2320
|
-
`, LEARNINGS_MD_TEMPLATE = `# Learnings
|
|
2321
|
-
|
|
2322
|
-
This file captures important lessons, decisions, and corrections made during development.
|
|
2323
|
-
It is read by AI agents before every task to avoid repeating mistakes and to follow established patterns.
|
|
2324
|
-
|
|
2325
|
-
<!-- Add learnings below this line. Format: - **[Category]**: Description -->
|
|
2326
2546
|
`, GITIGNORE_ENTRIES;
|
|
2327
2547
|
var init_init = __esm(() => {
|
|
2328
2548
|
init_config();
|
|
@@ -2330,9 +2550,10 @@ var init_init = __esm(() => {
|
|
|
2330
2550
|
init_ecosystem();
|
|
2331
2551
|
init_github();
|
|
2332
2552
|
init_logger();
|
|
2553
|
+
init_memory();
|
|
2333
2554
|
init_terminal();
|
|
2334
2555
|
init_types();
|
|
2335
|
-
GITIGNORE_ENTRIES = ["", "# Locus", ".locus/", "!.locus/
|
|
2556
|
+
GITIGNORE_ENTRIES = ["", "# Locus", ".locus/", "!.locus/memory/"];
|
|
2336
2557
|
});
|
|
2337
2558
|
|
|
2338
2559
|
// src/commands/create.ts
|
|
@@ -2341,8 +2562,8 @@ __export(exports_create, {
|
|
|
2341
2562
|
createCommand: () => createCommand
|
|
2342
2563
|
});
|
|
2343
2564
|
import { execSync as execSync5 } from "node:child_process";
|
|
2344
|
-
import { existsSync as
|
|
2345
|
-
import { join as
|
|
2565
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2566
|
+
import { join as join8 } from "node:path";
|
|
2346
2567
|
function validateName(name) {
|
|
2347
2568
|
if (!name)
|
|
2348
2569
|
return "Package name is required.";
|
|
@@ -2614,8 +2835,8 @@ ${bold2("Creating package:")} ${cyan2(fullNpmName)}
|
|
|
2614
2835
|
}
|
|
2615
2836
|
process.stderr.write(`${green("✓")} Name is valid: ${bold2(name)}
|
|
2616
2837
|
`);
|
|
2617
|
-
const packagesDir =
|
|
2618
|
-
if (
|
|
2838
|
+
const packagesDir = join8(process.cwd(), "packages", name);
|
|
2839
|
+
if (existsSync8(packagesDir)) {
|
|
2619
2840
|
process.stderr.write(`${red2("✗")} Directory already exists: ${bold2(`packages/${name}/`)}
|
|
2620
2841
|
`);
|
|
2621
2842
|
process.exit(1);
|
|
@@ -2632,37 +2853,37 @@ ${bold2("Creating package:")} ${cyan2(fullNpmName)}
|
|
|
2632
2853
|
let sdkVersion = "0.22.0";
|
|
2633
2854
|
let gatewayVersion = "0.22.0";
|
|
2634
2855
|
try {
|
|
2635
|
-
const { readFileSync:
|
|
2636
|
-
const sdkPkgPath =
|
|
2637
|
-
if (
|
|
2638
|
-
const sdkPkg = JSON.parse(
|
|
2856
|
+
const { readFileSync: readFileSync6 } = await import("node:fs");
|
|
2857
|
+
const sdkPkgPath = join8(process.cwd(), "packages", "sdk", "package.json");
|
|
2858
|
+
if (existsSync8(sdkPkgPath)) {
|
|
2859
|
+
const sdkPkg = JSON.parse(readFileSync6(sdkPkgPath, "utf-8"));
|
|
2639
2860
|
if (sdkPkg.version)
|
|
2640
2861
|
sdkVersion = sdkPkg.version;
|
|
2641
2862
|
}
|
|
2642
|
-
const gatewayPkgPath =
|
|
2643
|
-
if (
|
|
2644
|
-
const gatewayPkg = JSON.parse(
|
|
2863
|
+
const gatewayPkgPath = join8(process.cwd(), "packages", "gateway", "package.json");
|
|
2864
|
+
if (existsSync8(gatewayPkgPath)) {
|
|
2865
|
+
const gatewayPkg = JSON.parse(readFileSync6(gatewayPkgPath, "utf-8"));
|
|
2645
2866
|
if (gatewayPkg.version)
|
|
2646
2867
|
gatewayVersion = gatewayPkg.version;
|
|
2647
2868
|
}
|
|
2648
2869
|
} catch {}
|
|
2649
|
-
mkdirSync6(
|
|
2650
|
-
mkdirSync6(
|
|
2870
|
+
mkdirSync6(join8(packagesDir, "src"), { recursive: true });
|
|
2871
|
+
mkdirSync6(join8(packagesDir, "bin"), { recursive: true });
|
|
2651
2872
|
process.stderr.write(`${green("✓")} Created directory structure
|
|
2652
2873
|
`);
|
|
2653
|
-
writeFileSync5(
|
|
2874
|
+
writeFileSync5(join8(packagesDir, "package.json"), generatePackageJson(name, displayName, description, sdkVersion, gatewayVersion), "utf-8");
|
|
2654
2875
|
process.stderr.write(`${green("✓")} Generated package.json
|
|
2655
2876
|
`);
|
|
2656
|
-
writeFileSync5(
|
|
2877
|
+
writeFileSync5(join8(packagesDir, "tsconfig.json"), generateTsconfig(), "utf-8");
|
|
2657
2878
|
process.stderr.write(`${green("✓")} Generated tsconfig.json
|
|
2658
2879
|
`);
|
|
2659
|
-
writeFileSync5(
|
|
2880
|
+
writeFileSync5(join8(packagesDir, "src", "cli.ts"), generateCliTs(), "utf-8");
|
|
2660
2881
|
process.stderr.write(`${green("✓")} Generated src/cli.ts
|
|
2661
2882
|
`);
|
|
2662
|
-
writeFileSync5(
|
|
2883
|
+
writeFileSync5(join8(packagesDir, "src", "index.ts"), generateIndexTs(name), "utf-8");
|
|
2663
2884
|
process.stderr.write(`${green("✓")} Generated src/index.ts
|
|
2664
2885
|
`);
|
|
2665
|
-
writeFileSync5(
|
|
2886
|
+
writeFileSync5(join8(packagesDir, "README.md"), generateReadme(name, description), "utf-8");
|
|
2666
2887
|
process.stderr.write(`${green("✓")} Generated README.md
|
|
2667
2888
|
`);
|
|
2668
2889
|
process.stderr.write(`
|
|
@@ -2694,37 +2915,37 @@ var init_create = __esm(() => {
|
|
|
2694
2915
|
|
|
2695
2916
|
// src/packages/registry.ts
|
|
2696
2917
|
import {
|
|
2697
|
-
existsSync as
|
|
2918
|
+
existsSync as existsSync9,
|
|
2698
2919
|
mkdirSync as mkdirSync7,
|
|
2699
|
-
readFileSync as
|
|
2920
|
+
readFileSync as readFileSync6,
|
|
2700
2921
|
renameSync,
|
|
2701
2922
|
writeFileSync as writeFileSync6
|
|
2702
2923
|
} from "node:fs";
|
|
2703
2924
|
import { homedir as homedir2 } from "node:os";
|
|
2704
|
-
import { join as
|
|
2925
|
+
import { join as join9 } from "node:path";
|
|
2705
2926
|
function getPackagesDir() {
|
|
2706
2927
|
const home = process.env.HOME || homedir2();
|
|
2707
|
-
const dir =
|
|
2708
|
-
if (!
|
|
2928
|
+
const dir = join9(home, ".locus", "packages");
|
|
2929
|
+
if (!existsSync9(dir)) {
|
|
2709
2930
|
mkdirSync7(dir, { recursive: true });
|
|
2710
2931
|
}
|
|
2711
|
-
const pkgJson =
|
|
2712
|
-
if (!
|
|
2932
|
+
const pkgJson = join9(dir, "package.json");
|
|
2933
|
+
if (!existsSync9(pkgJson)) {
|
|
2713
2934
|
writeFileSync6(pkgJson, `${JSON.stringify({ private: true }, null, 2)}
|
|
2714
2935
|
`, "utf-8");
|
|
2715
2936
|
}
|
|
2716
2937
|
return dir;
|
|
2717
2938
|
}
|
|
2718
2939
|
function getRegistryPath() {
|
|
2719
|
-
return
|
|
2940
|
+
return join9(getPackagesDir(), "registry.json");
|
|
2720
2941
|
}
|
|
2721
2942
|
function loadRegistry() {
|
|
2722
2943
|
const registryPath = getRegistryPath();
|
|
2723
|
-
if (!
|
|
2944
|
+
if (!existsSync9(registryPath)) {
|
|
2724
2945
|
return { packages: {} };
|
|
2725
2946
|
}
|
|
2726
2947
|
try {
|
|
2727
|
-
const raw =
|
|
2948
|
+
const raw = readFileSync6(registryPath, "utf-8");
|
|
2728
2949
|
const parsed = JSON.parse(raw);
|
|
2729
2950
|
if (typeof parsed === "object" && parsed !== null && "packages" in parsed && typeof parsed.packages === "object") {
|
|
2730
2951
|
const registry = parsed;
|
|
@@ -2758,8 +2979,8 @@ function saveRegistry(registry) {
|
|
|
2758
2979
|
function resolvePackageBinary(packageName) {
|
|
2759
2980
|
const fullName = normalizePackageName(packageName);
|
|
2760
2981
|
const binName = fullName.includes("/") ? fullName.split("/").pop() : fullName;
|
|
2761
|
-
const binPath =
|
|
2762
|
-
return
|
|
2982
|
+
const binPath = join9(getPackagesDir(), "node_modules", ".bin", binName);
|
|
2983
|
+
return existsSync9(binPath) ? binPath : null;
|
|
2763
2984
|
}
|
|
2764
2985
|
function normalizePackageName(input) {
|
|
2765
2986
|
if (input.startsWith(SCOPED_PREFIX)) {
|
|
@@ -2785,7 +3006,7 @@ __export(exports_pkg, {
|
|
|
2785
3006
|
listInstalledPackages: () => listInstalledPackages
|
|
2786
3007
|
});
|
|
2787
3008
|
import { spawn } from "node:child_process";
|
|
2788
|
-
import { existsSync as
|
|
3009
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
2789
3010
|
function listInstalledPackages() {
|
|
2790
3011
|
const registry = loadRegistry();
|
|
2791
3012
|
const entries = Object.values(registry.packages);
|
|
@@ -2864,7 +3085,7 @@ async function pkgCommand(args, _flags) {
|
|
|
2864
3085
|
return;
|
|
2865
3086
|
}
|
|
2866
3087
|
let binaryPath = entry.binaryPath;
|
|
2867
|
-
if (!binaryPath || !
|
|
3088
|
+
if (!binaryPath || !existsSync10(binaryPath)) {
|
|
2868
3089
|
const resolved = resolvePackageBinary(packageName);
|
|
2869
3090
|
if (resolved) {
|
|
2870
3091
|
binaryPath = resolved;
|
|
@@ -2997,8 +3218,8 @@ __export(exports_install, {
|
|
|
2997
3218
|
installCommand: () => installCommand
|
|
2998
3219
|
});
|
|
2999
3220
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
3000
|
-
import { existsSync as
|
|
3001
|
-
import { join as
|
|
3221
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
|
|
3222
|
+
import { join as join10 } from "node:path";
|
|
3002
3223
|
function parsePackageArg(raw) {
|
|
3003
3224
|
if (raw.startsWith("@")) {
|
|
3004
3225
|
const slashIdx = raw.indexOf("/");
|
|
@@ -3075,8 +3296,8 @@ ${red2("✗")} Failed to install ${bold2(packageSpec)}.
|
|
|
3075
3296
|
process.exit(1);
|
|
3076
3297
|
return;
|
|
3077
3298
|
}
|
|
3078
|
-
const installedPkgJsonPath =
|
|
3079
|
-
if (!
|
|
3299
|
+
const installedPkgJsonPath = join10(packagesDir, "node_modules", packageName, "package.json");
|
|
3300
|
+
if (!existsSync11(installedPkgJsonPath)) {
|
|
3080
3301
|
process.stderr.write(`
|
|
3081
3302
|
${red2("✗")} Package installed but package.json not found at:
|
|
3082
3303
|
`);
|
|
@@ -3087,7 +3308,7 @@ ${red2("✗")} Package installed but package.json not found at:
|
|
|
3087
3308
|
}
|
|
3088
3309
|
let installedPkgJson;
|
|
3089
3310
|
try {
|
|
3090
|
-
installedPkgJson = JSON.parse(
|
|
3311
|
+
installedPkgJson = JSON.parse(readFileSync7(installedPkgJsonPath, "utf-8"));
|
|
3091
3312
|
} catch {
|
|
3092
3313
|
process.stderr.write(`
|
|
3093
3314
|
${red2("✗")} Could not parse installed package.json.
|
|
@@ -3329,19 +3550,19 @@ var REGISTRY_REPO = "asgarovf/locusai", REGISTRY_BRANCH = "master", SKILLS_LOCK_
|
|
|
3329
3550
|
|
|
3330
3551
|
// src/skills/lock.ts
|
|
3331
3552
|
import { createHash } from "node:crypto";
|
|
3332
|
-
import { readFileSync as
|
|
3333
|
-
import { join as
|
|
3553
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3554
|
+
import { join as join11 } from "node:path";
|
|
3334
3555
|
function readLockFile(projectRoot) {
|
|
3335
|
-
const filePath =
|
|
3556
|
+
const filePath = join11(projectRoot, SKILLS_LOCK_FILENAME);
|
|
3336
3557
|
try {
|
|
3337
|
-
const raw =
|
|
3558
|
+
const raw = readFileSync8(filePath, "utf-8");
|
|
3338
3559
|
return JSON.parse(raw);
|
|
3339
3560
|
} catch {
|
|
3340
3561
|
return { ...DEFAULT_LOCK_FILE, skills: {} };
|
|
3341
3562
|
}
|
|
3342
3563
|
}
|
|
3343
3564
|
function writeLockFile(projectRoot, lockFile) {
|
|
3344
|
-
const filePath =
|
|
3565
|
+
const filePath = join11(projectRoot, SKILLS_LOCK_FILENAME);
|
|
3345
3566
|
writeFileSync7(filePath, `${JSON.stringify(lockFile, null, 2)}
|
|
3346
3567
|
`, "utf-8");
|
|
3347
3568
|
}
|
|
@@ -3355,29 +3576,29 @@ var init_lock = __esm(() => {
|
|
|
3355
3576
|
|
|
3356
3577
|
// src/skills/installer.ts
|
|
3357
3578
|
import {
|
|
3358
|
-
existsSync as
|
|
3579
|
+
existsSync as existsSync12,
|
|
3359
3580
|
mkdirSync as mkdirSync8,
|
|
3360
|
-
readFileSync as
|
|
3581
|
+
readFileSync as readFileSync9,
|
|
3361
3582
|
renameSync as renameSync2,
|
|
3362
3583
|
rmSync,
|
|
3363
3584
|
writeFileSync as writeFileSync8
|
|
3364
3585
|
} from "node:fs";
|
|
3365
3586
|
import { tmpdir } from "node:os";
|
|
3366
|
-
import { join as
|
|
3587
|
+
import { join as join12 } from "node:path";
|
|
3367
3588
|
async function installSkill(projectRoot, name, content, source) {
|
|
3368
|
-
const claudeDir =
|
|
3369
|
-
const agentsDir =
|
|
3370
|
-
const stagingDir =
|
|
3371
|
-
const stagingClaudeDir =
|
|
3372
|
-
const stagingAgentsDir =
|
|
3589
|
+
const claudeDir = join12(projectRoot, CLAUDE_SKILLS_DIR, name);
|
|
3590
|
+
const agentsDir = join12(projectRoot, AGENTS_SKILLS_DIR, name);
|
|
3591
|
+
const stagingDir = join12(tmpdir(), `locus-skill-${name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
3592
|
+
const stagingClaudeDir = join12(stagingDir, "claude");
|
|
3593
|
+
const stagingAgentsDir = join12(stagingDir, "agents");
|
|
3373
3594
|
let wroteClaudeDir = false;
|
|
3374
3595
|
let wroteAgentsDir = false;
|
|
3375
3596
|
try {
|
|
3376
3597
|
try {
|
|
3377
3598
|
mkdirSync8(stagingClaudeDir, { recursive: true });
|
|
3378
3599
|
mkdirSync8(stagingAgentsDir, { recursive: true });
|
|
3379
|
-
writeFileSync8(
|
|
3380
|
-
writeFileSync8(
|
|
3600
|
+
writeFileSync8(join12(stagingClaudeDir, "SKILL.md"), content, "utf-8");
|
|
3601
|
+
writeFileSync8(join12(stagingAgentsDir, "SKILL.md"), content, "utf-8");
|
|
3381
3602
|
} catch (err) {
|
|
3382
3603
|
throw new SkillInstallError(name, "stage", err);
|
|
3383
3604
|
}
|
|
@@ -3386,7 +3607,7 @@ async function installSkill(projectRoot, name, content, source) {
|
|
|
3386
3607
|
throw new Error("Skill content is empty");
|
|
3387
3608
|
}
|
|
3388
3609
|
const expectedHash = computeSkillHash(content);
|
|
3389
|
-
const stagedContent =
|
|
3610
|
+
const stagedContent = readFileSync9(join12(stagingClaudeDir, "SKILL.md"), "utf-8");
|
|
3390
3611
|
const stagedHash = computeSkillHash(stagedContent);
|
|
3391
3612
|
if (stagedHash !== expectedHash) {
|
|
3392
3613
|
throw new Error("Staged file hash does not match expected content");
|
|
@@ -3397,12 +3618,12 @@ async function installSkill(projectRoot, name, content, source) {
|
|
|
3397
3618
|
throw new SkillInstallError(name, "validate", err);
|
|
3398
3619
|
}
|
|
3399
3620
|
try {
|
|
3400
|
-
mkdirSync8(
|
|
3401
|
-
mkdirSync8(
|
|
3402
|
-
if (
|
|
3621
|
+
mkdirSync8(join12(projectRoot, CLAUDE_SKILLS_DIR), { recursive: true });
|
|
3622
|
+
mkdirSync8(join12(projectRoot, AGENTS_SKILLS_DIR), { recursive: true });
|
|
3623
|
+
if (existsSync12(claudeDir)) {
|
|
3403
3624
|
rmSync(claudeDir, { recursive: true, force: true });
|
|
3404
3625
|
}
|
|
3405
|
-
if (
|
|
3626
|
+
if (existsSync12(agentsDir)) {
|
|
3406
3627
|
rmSync(agentsDir, { recursive: true, force: true });
|
|
3407
3628
|
}
|
|
3408
3629
|
try {
|
|
@@ -3410,7 +3631,7 @@ async function installSkill(projectRoot, name, content, source) {
|
|
|
3410
3631
|
wroteClaudeDir = true;
|
|
3411
3632
|
} catch {
|
|
3412
3633
|
mkdirSync8(claudeDir, { recursive: true });
|
|
3413
|
-
writeFileSync8(
|
|
3634
|
+
writeFileSync8(join12(claudeDir, "SKILL.md"), content, "utf-8");
|
|
3414
3635
|
wroteClaudeDir = true;
|
|
3415
3636
|
}
|
|
3416
3637
|
try {
|
|
@@ -3418,7 +3639,7 @@ async function installSkill(projectRoot, name, content, source) {
|
|
|
3418
3639
|
wroteAgentsDir = true;
|
|
3419
3640
|
} catch {
|
|
3420
3641
|
mkdirSync8(agentsDir, { recursive: true });
|
|
3421
|
-
writeFileSync8(
|
|
3642
|
+
writeFileSync8(join12(agentsDir, "SKILL.md"), content, "utf-8");
|
|
3422
3643
|
wroteAgentsDir = true;
|
|
3423
3644
|
}
|
|
3424
3645
|
} catch (err) {
|
|
@@ -3438,26 +3659,26 @@ async function installSkill(projectRoot, name, content, source) {
|
|
|
3438
3659
|
throw new SkillInstallError(name, "register", err);
|
|
3439
3660
|
}
|
|
3440
3661
|
} catch (err) {
|
|
3441
|
-
if (wroteClaudeDir &&
|
|
3662
|
+
if (wroteClaudeDir && existsSync12(claudeDir)) {
|
|
3442
3663
|
rmSync(claudeDir, { recursive: true, force: true });
|
|
3443
3664
|
}
|
|
3444
|
-
if (wroteAgentsDir &&
|
|
3665
|
+
if (wroteAgentsDir && existsSync12(agentsDir)) {
|
|
3445
3666
|
rmSync(agentsDir, { recursive: true, force: true });
|
|
3446
3667
|
}
|
|
3447
3668
|
throw err;
|
|
3448
3669
|
} finally {
|
|
3449
|
-
if (
|
|
3670
|
+
if (existsSync12(stagingDir)) {
|
|
3450
3671
|
rmSync(stagingDir, { recursive: true, force: true });
|
|
3451
3672
|
}
|
|
3452
3673
|
}
|
|
3453
3674
|
}
|
|
3454
3675
|
async function removeSkill(projectRoot, name) {
|
|
3455
|
-
const claudeDir =
|
|
3456
|
-
const agentsDir =
|
|
3676
|
+
const claudeDir = join12(projectRoot, CLAUDE_SKILLS_DIR, name);
|
|
3677
|
+
const agentsDir = join12(projectRoot, AGENTS_SKILLS_DIR, name);
|
|
3457
3678
|
const lock = readLockFile(projectRoot);
|
|
3458
3679
|
const inLockFile = name in lock.skills;
|
|
3459
|
-
const hasClaude =
|
|
3460
|
-
const hasAgents =
|
|
3680
|
+
const hasClaude = existsSync12(claudeDir);
|
|
3681
|
+
const hasAgents = existsSync12(agentsDir);
|
|
3461
3682
|
if (!inLockFile && !hasClaude && !hasAgents) {
|
|
3462
3683
|
console.warn(`Skill "${name}" is not installed.`);
|
|
3463
3684
|
return;
|
|
@@ -3478,10 +3699,10 @@ function isSkillInstalled(projectRoot, name) {
|
|
|
3478
3699
|
return name in lock.skills;
|
|
3479
3700
|
}
|
|
3480
3701
|
function hasOrphanedSkillFiles(projectRoot, name) {
|
|
3481
|
-
const claudeDir =
|
|
3482
|
-
const agentsDir =
|
|
3702
|
+
const claudeDir = join12(projectRoot, CLAUDE_SKILLS_DIR, name);
|
|
3703
|
+
const agentsDir = join12(projectRoot, AGENTS_SKILLS_DIR, name);
|
|
3483
3704
|
const inLockFile = isSkillInstalled(projectRoot, name);
|
|
3484
|
-
return !inLockFile && (
|
|
3705
|
+
return !inLockFile && (existsSync12(claudeDir) || existsSync12(agentsDir));
|
|
3485
3706
|
}
|
|
3486
3707
|
var SkillInstallError;
|
|
3487
3708
|
var init_installer = __esm(() => {
|
|
@@ -4092,16 +4313,16 @@ __export(exports_logs, {
|
|
|
4092
4313
|
logsCommand: () => logsCommand
|
|
4093
4314
|
});
|
|
4094
4315
|
import {
|
|
4095
|
-
existsSync as
|
|
4316
|
+
existsSync as existsSync13,
|
|
4096
4317
|
readdirSync as readdirSync2,
|
|
4097
|
-
readFileSync as
|
|
4318
|
+
readFileSync as readFileSync10,
|
|
4098
4319
|
statSync as statSync2,
|
|
4099
4320
|
unlinkSync as unlinkSync2
|
|
4100
4321
|
} from "node:fs";
|
|
4101
|
-
import { join as
|
|
4322
|
+
import { join as join13 } from "node:path";
|
|
4102
4323
|
async function logsCommand(cwd, options) {
|
|
4103
|
-
const logsDir =
|
|
4104
|
-
if (!
|
|
4324
|
+
const logsDir = join13(cwd, ".locus", "logs");
|
|
4325
|
+
if (!existsSync13(logsDir)) {
|
|
4105
4326
|
process.stderr.write(`${dim2("No logs found.")}
|
|
4106
4327
|
`);
|
|
4107
4328
|
return;
|
|
@@ -4121,7 +4342,7 @@ async function logsCommand(cwd, options) {
|
|
|
4121
4342
|
return viewLog(logFiles[0], options.level, options.lines ?? 50);
|
|
4122
4343
|
}
|
|
4123
4344
|
function viewLog(logFile, levelFilter, maxLines) {
|
|
4124
|
-
const content =
|
|
4345
|
+
const content = readFileSync10(logFile, "utf-8");
|
|
4125
4346
|
const lines = content.trim().split(`
|
|
4126
4347
|
`).filter(Boolean);
|
|
4127
4348
|
process.stderr.write(`
|
|
@@ -4156,9 +4377,9 @@ async function tailLog(logFile, levelFilter) {
|
|
|
4156
4377
|
process.stderr.write(`${bold2("Tailing:")} ${dim2(logFile)} ${dim2("(Ctrl+C to stop)")}
|
|
4157
4378
|
|
|
4158
4379
|
`);
|
|
4159
|
-
let lastSize =
|
|
4160
|
-
if (
|
|
4161
|
-
const content =
|
|
4380
|
+
let lastSize = existsSync13(logFile) ? statSync2(logFile).size : 0;
|
|
4381
|
+
if (existsSync13(logFile)) {
|
|
4382
|
+
const content = readFileSync10(logFile, "utf-8");
|
|
4162
4383
|
const lines = content.trim().split(`
|
|
4163
4384
|
`).filter(Boolean);
|
|
4164
4385
|
const recent = lines.slice(-10);
|
|
@@ -4176,12 +4397,12 @@ async function tailLog(logFile, levelFilter) {
|
|
|
4176
4397
|
}
|
|
4177
4398
|
return new Promise((resolve) => {
|
|
4178
4399
|
const interval = setInterval(() => {
|
|
4179
|
-
if (!
|
|
4400
|
+
if (!existsSync13(logFile))
|
|
4180
4401
|
return;
|
|
4181
4402
|
const currentSize = statSync2(logFile).size;
|
|
4182
4403
|
if (currentSize <= lastSize)
|
|
4183
4404
|
return;
|
|
4184
|
-
const content =
|
|
4405
|
+
const content = readFileSync10(logFile, "utf-8");
|
|
4185
4406
|
const allLines = content.trim().split(`
|
|
4186
4407
|
`).filter(Boolean);
|
|
4187
4408
|
const oldContent = content.slice(0, lastSize);
|
|
@@ -4236,7 +4457,7 @@ function cleanLogs(logsDir) {
|
|
|
4236
4457
|
`);
|
|
4237
4458
|
}
|
|
4238
4459
|
function getLogFiles(logsDir) {
|
|
4239
|
-
return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) =>
|
|
4460
|
+
return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) => join13(logsDir, f)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
|
|
4240
4461
|
}
|
|
4241
4462
|
function formatEntry(entry) {
|
|
4242
4463
|
const time = dim2(new Date(entry.ts).toLocaleTimeString());
|
|
@@ -4546,9 +4767,9 @@ var init_stream_renderer = __esm(() => {
|
|
|
4546
4767
|
|
|
4547
4768
|
// src/repl/clipboard.ts
|
|
4548
4769
|
import { execSync as execSync6 } from "node:child_process";
|
|
4549
|
-
import { existsSync as
|
|
4770
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync9 } from "node:fs";
|
|
4550
4771
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
4551
|
-
import { join as
|
|
4772
|
+
import { join as join14 } from "node:path";
|
|
4552
4773
|
function readClipboardImage() {
|
|
4553
4774
|
if (process.platform === "darwin") {
|
|
4554
4775
|
return readMacOSClipboardImage();
|
|
@@ -4559,14 +4780,14 @@ function readClipboardImage() {
|
|
|
4559
4780
|
return null;
|
|
4560
4781
|
}
|
|
4561
4782
|
function ensureStableDir() {
|
|
4562
|
-
if (!
|
|
4783
|
+
if (!existsSync14(STABLE_DIR)) {
|
|
4563
4784
|
mkdirSync9(STABLE_DIR, { recursive: true });
|
|
4564
4785
|
}
|
|
4565
4786
|
}
|
|
4566
4787
|
function readMacOSClipboardImage() {
|
|
4567
4788
|
try {
|
|
4568
4789
|
ensureStableDir();
|
|
4569
|
-
const destPath =
|
|
4790
|
+
const destPath = join14(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
4570
4791
|
const script = [
|
|
4571
4792
|
`set destPath to POSIX file "${destPath}"`,
|
|
4572
4793
|
"try",
|
|
@@ -4590,7 +4811,7 @@ function readMacOSClipboardImage() {
|
|
|
4590
4811
|
timeout: 5000,
|
|
4591
4812
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4592
4813
|
}).trim();
|
|
4593
|
-
if (result === "ok" &&
|
|
4814
|
+
if (result === "ok" && existsSync14(destPath)) {
|
|
4594
4815
|
return destPath;
|
|
4595
4816
|
}
|
|
4596
4817
|
} catch {}
|
|
@@ -4603,9 +4824,9 @@ function readLinuxClipboardImage() {
|
|
|
4603
4824
|
return null;
|
|
4604
4825
|
}
|
|
4605
4826
|
ensureStableDir();
|
|
4606
|
-
const destPath =
|
|
4827
|
+
const destPath = join14(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
4607
4828
|
execSync6(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
|
|
4608
|
-
if (
|
|
4829
|
+
if (existsSync14(destPath)) {
|
|
4609
4830
|
return destPath;
|
|
4610
4831
|
}
|
|
4611
4832
|
} catch {}
|
|
@@ -4613,13 +4834,13 @@ function readLinuxClipboardImage() {
|
|
|
4613
4834
|
}
|
|
4614
4835
|
var STABLE_DIR;
|
|
4615
4836
|
var init_clipboard = __esm(() => {
|
|
4616
|
-
STABLE_DIR =
|
|
4837
|
+
STABLE_DIR = join14(tmpdir2(), "locus-images");
|
|
4617
4838
|
});
|
|
4618
4839
|
|
|
4619
4840
|
// src/repl/image-detect.ts
|
|
4620
|
-
import { copyFileSync, existsSync as
|
|
4841
|
+
import { copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "node:fs";
|
|
4621
4842
|
import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
|
|
4622
|
-
import { basename, extname, join as
|
|
4843
|
+
import { basename, extname, join as join15, resolve } from "node:path";
|
|
4623
4844
|
function detectImages(input) {
|
|
4624
4845
|
const detected = [];
|
|
4625
4846
|
const byResolved = new Map;
|
|
@@ -4713,15 +4934,15 @@ function collectReferencedAttachments(input, attachments) {
|
|
|
4713
4934
|
return dedupeByResolvedPath(selected);
|
|
4714
4935
|
}
|
|
4715
4936
|
function relocateImages(images, projectRoot) {
|
|
4716
|
-
const targetDir =
|
|
4937
|
+
const targetDir = join15(projectRoot, ".locus", "tmp", "images");
|
|
4717
4938
|
for (const img of images) {
|
|
4718
4939
|
if (!img.exists)
|
|
4719
4940
|
continue;
|
|
4720
4941
|
try {
|
|
4721
|
-
if (!
|
|
4942
|
+
if (!existsSync15(targetDir)) {
|
|
4722
4943
|
mkdirSync10(targetDir, { recursive: true });
|
|
4723
4944
|
}
|
|
4724
|
-
const dest =
|
|
4945
|
+
const dest = join15(targetDir, basename(img.stablePath));
|
|
4725
4946
|
copyFileSync(img.stablePath, dest);
|
|
4726
4947
|
img.stablePath = dest;
|
|
4727
4948
|
} catch {}
|
|
@@ -4733,7 +4954,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
4733
4954
|
return;
|
|
4734
4955
|
let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
|
|
4735
4956
|
if (resolved.startsWith("~/")) {
|
|
4736
|
-
resolved =
|
|
4957
|
+
resolved = join15(homedir3(), resolved.slice(2));
|
|
4737
4958
|
}
|
|
4738
4959
|
resolved = resolve(resolved);
|
|
4739
4960
|
const existing = byResolved.get(resolved);
|
|
@@ -4746,7 +4967,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
4746
4967
|
]);
|
|
4747
4968
|
return;
|
|
4748
4969
|
}
|
|
4749
|
-
const exists =
|
|
4970
|
+
const exists = existsSync15(resolved);
|
|
4750
4971
|
let stablePath = resolved;
|
|
4751
4972
|
if (exists) {
|
|
4752
4973
|
stablePath = copyToStable(resolved);
|
|
@@ -4800,10 +5021,10 @@ function dedupeByResolvedPath(images) {
|
|
|
4800
5021
|
}
|
|
4801
5022
|
function copyToStable(sourcePath) {
|
|
4802
5023
|
try {
|
|
4803
|
-
if (!
|
|
5024
|
+
if (!existsSync15(STABLE_DIR2)) {
|
|
4804
5025
|
mkdirSync10(STABLE_DIR2, { recursive: true });
|
|
4805
5026
|
}
|
|
4806
|
-
const dest =
|
|
5027
|
+
const dest = join15(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
|
|
4807
5028
|
copyFileSync(sourcePath, dest);
|
|
4808
5029
|
return dest;
|
|
4809
5030
|
} catch {
|
|
@@ -4823,7 +5044,7 @@ var init_image_detect = __esm(() => {
|
|
|
4823
5044
|
".tif",
|
|
4824
5045
|
".tiff"
|
|
4825
5046
|
]);
|
|
4826
|
-
STABLE_DIR2 =
|
|
5047
|
+
STABLE_DIR2 = join15(tmpdir3(), "locus-images");
|
|
4827
5048
|
PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
|
|
4828
5049
|
});
|
|
4829
5050
|
|
|
@@ -5832,21 +6053,21 @@ var init_claude = __esm(() => {
|
|
|
5832
6053
|
import { exec } from "node:child_process";
|
|
5833
6054
|
import {
|
|
5834
6055
|
cpSync,
|
|
5835
|
-
existsSync as
|
|
6056
|
+
existsSync as existsSync16,
|
|
5836
6057
|
mkdirSync as mkdirSync11,
|
|
5837
6058
|
mkdtempSync,
|
|
5838
6059
|
readdirSync as readdirSync3,
|
|
5839
|
-
readFileSync as
|
|
6060
|
+
readFileSync as readFileSync11,
|
|
5840
6061
|
rmSync as rmSync2,
|
|
5841
6062
|
statSync as statSync3
|
|
5842
6063
|
} from "node:fs";
|
|
5843
6064
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
5844
|
-
import { dirname as dirname3, join as
|
|
6065
|
+
import { dirname as dirname3, join as join16, relative } from "node:path";
|
|
5845
6066
|
import { promisify } from "node:util";
|
|
5846
6067
|
function parseIgnoreFile(filePath) {
|
|
5847
|
-
if (!
|
|
6068
|
+
if (!existsSync16(filePath))
|
|
5848
6069
|
return [];
|
|
5849
|
-
const content =
|
|
6070
|
+
const content = readFileSync11(filePath, "utf-8");
|
|
5850
6071
|
const rules = [];
|
|
5851
6072
|
for (const rawLine of content.split(`
|
|
5852
6073
|
`)) {
|
|
@@ -5914,14 +6135,14 @@ function findIgnoredPaths(projectRoot, rules) {
|
|
|
5914
6135
|
for (const name of entries) {
|
|
5915
6136
|
if (SKIP_DIRS.has(name))
|
|
5916
6137
|
continue;
|
|
5917
|
-
const fullPath =
|
|
5918
|
-
let
|
|
6138
|
+
const fullPath = join16(dir, name);
|
|
6139
|
+
let stat2 = null;
|
|
5919
6140
|
try {
|
|
5920
|
-
|
|
6141
|
+
stat2 = statSync3(fullPath);
|
|
5921
6142
|
} catch {
|
|
5922
6143
|
continue;
|
|
5923
6144
|
}
|
|
5924
|
-
const isDir =
|
|
6145
|
+
const isDir = stat2.isDirectory();
|
|
5925
6146
|
let matched = false;
|
|
5926
6147
|
for (const m of positiveMatchers) {
|
|
5927
6148
|
if (m.isDirectory && !isDir)
|
|
@@ -5948,7 +6169,7 @@ function findIgnoredPaths(projectRoot, rules) {
|
|
|
5948
6169
|
}
|
|
5949
6170
|
function backupIgnoredFiles(projectRoot) {
|
|
5950
6171
|
const log = getLogger();
|
|
5951
|
-
const ignorePath =
|
|
6172
|
+
const ignorePath = join16(projectRoot, ".sandboxignore");
|
|
5952
6173
|
const rules = parseIgnoreFile(ignorePath);
|
|
5953
6174
|
if (rules.length === 0)
|
|
5954
6175
|
return NOOP_BACKUP;
|
|
@@ -5957,7 +6178,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5957
6178
|
return NOOP_BACKUP;
|
|
5958
6179
|
let backupDir;
|
|
5959
6180
|
try {
|
|
5960
|
-
backupDir = mkdtempSync(
|
|
6181
|
+
backupDir = mkdtempSync(join16(tmpdir4(), "locus-sandbox-backup-"));
|
|
5961
6182
|
} catch (err) {
|
|
5962
6183
|
log.debug("Failed to create sandbox backup dir", {
|
|
5963
6184
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -5967,7 +6188,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5967
6188
|
const backed = [];
|
|
5968
6189
|
for (const src of paths) {
|
|
5969
6190
|
const rel = relative(projectRoot, src);
|
|
5970
|
-
const dest =
|
|
6191
|
+
const dest = join16(backupDir, rel);
|
|
5971
6192
|
try {
|
|
5972
6193
|
mkdirSync11(dirname3(dest), { recursive: true });
|
|
5973
6194
|
cpSync(src, dest, { recursive: true, preserveTimestamps: true });
|
|
@@ -6009,7 +6230,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
6009
6230
|
}
|
|
6010
6231
|
async function enforceSandboxIgnore(sandboxName, projectRoot, containerWorkdir) {
|
|
6011
6232
|
const log = getLogger();
|
|
6012
|
-
const ignorePath =
|
|
6233
|
+
const ignorePath = join16(projectRoot, ".sandboxignore");
|
|
6013
6234
|
const rules = parseIgnoreFile(ignorePath);
|
|
6014
6235
|
if (rules.length === 0)
|
|
6015
6236
|
return;
|
|
@@ -8175,8 +8396,8 @@ var init_sprint = __esm(() => {
|
|
|
8175
8396
|
|
|
8176
8397
|
// src/core/prompt-builder.ts
|
|
8177
8398
|
import { execSync as execSync9 } from "node:child_process";
|
|
8178
|
-
import { existsSync as
|
|
8179
|
-
import { join as
|
|
8399
|
+
import { existsSync as existsSync17, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "node:fs";
|
|
8400
|
+
import { join as join17 } from "node:path";
|
|
8180
8401
|
function buildExecutionPrompt(ctx) {
|
|
8181
8402
|
const sections = [];
|
|
8182
8403
|
sections.push(buildSystemContext(ctx.projectRoot));
|
|
@@ -8212,15 +8433,22 @@ function buildFeedbackPrompt(ctx) {
|
|
|
8212
8433
|
}
|
|
8213
8434
|
function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
|
|
8214
8435
|
const sections = [];
|
|
8215
|
-
const locusmd = readFileSafe(
|
|
8436
|
+
const locusmd = readFileSafe(join17(projectRoot, ".locus", "LOCUS.md"));
|
|
8216
8437
|
if (locusmd) {
|
|
8217
8438
|
sections.push(`<project-instructions>
|
|
8218
8439
|
${locusmd}
|
|
8219
8440
|
</project-instructions>`);
|
|
8220
8441
|
}
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8442
|
+
const memory = loadMemoryContent(projectRoot);
|
|
8443
|
+
if (memory) {
|
|
8444
|
+
sections.push(`<past-learnings>
|
|
8445
|
+
${memory}
|
|
8446
|
+
</past-learnings>`);
|
|
8447
|
+
} else {
|
|
8448
|
+
sections.push(`<past-learnings>
|
|
8449
|
+
No past learnings recorded yet.</past-learnings>`);
|
|
8450
|
+
}
|
|
8451
|
+
sections.push(`<learnings-reminder>IMPORTANT: If during this interaction you discover reusable lessons (architectural patterns, non-obvious constraints, user corrections), record them in the appropriate category file in \`.locus/memory/\` before finishing. This is mandatory — see the "Continuous Learning" section in project instructions.</learnings-reminder>`);
|
|
8224
8452
|
if (previousMessages && previousMessages.length > 0) {
|
|
8225
8453
|
const recent = previousMessages.slice(-10);
|
|
8226
8454
|
const historyLines = recent.map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`);
|
|
@@ -8239,22 +8467,41 @@ ${userMessage}
|
|
|
8239
8467
|
|
|
8240
8468
|
`);
|
|
8241
8469
|
}
|
|
8470
|
+
function loadMemoryContent(projectRoot) {
|
|
8471
|
+
const memoryDir = getMemoryDir(projectRoot);
|
|
8472
|
+
if (existsSync17(memoryDir)) {
|
|
8473
|
+
const content = readAllMemorySync(projectRoot);
|
|
8474
|
+
if (content.trim()) {
|
|
8475
|
+
return content.length > MEMORY_MAX_CHARS ? `${content.slice(0, MEMORY_MAX_CHARS)}
|
|
8476
|
+
|
|
8477
|
+
...(truncated)` : content;
|
|
8478
|
+
}
|
|
8479
|
+
}
|
|
8480
|
+
return "";
|
|
8481
|
+
}
|
|
8242
8482
|
function buildSystemContext(projectRoot) {
|
|
8243
8483
|
const parts = [];
|
|
8244
|
-
const locusmd = readFileSafe(
|
|
8484
|
+
const locusmd = readFileSafe(join17(projectRoot, ".locus", "LOCUS.md"));
|
|
8245
8485
|
if (locusmd) {
|
|
8246
8486
|
parts.push(`<project-instructions>
|
|
8247
8487
|
${locusmd}
|
|
8248
8488
|
</project-instructions>`);
|
|
8249
8489
|
}
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8490
|
+
const memory = loadMemoryContent(projectRoot);
|
|
8491
|
+
if (memory) {
|
|
8492
|
+
parts.push(`<past-learnings>
|
|
8493
|
+
${memory}
|
|
8494
|
+
</past-learnings>`);
|
|
8495
|
+
} else {
|
|
8496
|
+
parts.push(`<past-learnings>
|
|
8497
|
+
No past learnings recorded yet.</past-learnings>`);
|
|
8498
|
+
}
|
|
8499
|
+
const discussionsDir = join17(projectRoot, ".locus", "discussions");
|
|
8500
|
+
if (existsSync17(discussionsDir)) {
|
|
8254
8501
|
try {
|
|
8255
8502
|
const files = readdirSync4(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
|
|
8256
8503
|
for (const file of files) {
|
|
8257
|
-
const content = readFileSafe(
|
|
8504
|
+
const content = readFileSafe(join17(discussionsDir, file));
|
|
8258
8505
|
if (content) {
|
|
8259
8506
|
const name = file.replace(".md", "");
|
|
8260
8507
|
parts.push(`<discussion name="${name}">
|
|
@@ -8363,7 +8610,7 @@ function buildExecutionRules(config) {
|
|
|
8363
8610
|
1. **Commit format:** Use conventional commits: \`feat: <title> (#<issue>)\`, \`fix: ...\`, \`chore: ...\`. Every commit message MUST be multi-line: the first line is the title, then a blank line, then \`Co-Authored-By: LocusAgent <agent@locusai.team>\` as a Git trailer. Use \`git commit -m "<title>" -m "Co-Authored-By: LocusAgent <agent@locusai.team>"\` (two separate -m flags) to ensure the trailer is on its own line.
|
|
8364
8611
|
2. **Code quality:** Follow existing code style. Run linters/formatters if available.
|
|
8365
8612
|
3. **Testing:** If test files exist for modified code, update them accordingly.
|
|
8366
|
-
4. **Update
|
|
8613
|
+
4. **Update memory:** Before finishing, if you discovered any reusable lessons (architectural patterns, non-obvious constraints, user corrections), record them in the appropriate category file in \`.locus/memory/\` (architecture.md, conventions.md, decisions.md, preferences.md, debugging.md). This is mandatory — see the "Continuous Learning" section in project instructions.
|
|
8367
8614
|
5. **Do NOT:**
|
|
8368
8615
|
- Run \`git push\` (the orchestrator handles pushing)
|
|
8369
8616
|
- Modify files outside the scope of this issue
|
|
@@ -8401,13 +8648,13 @@ function buildFeedbackInstructions() {
|
|
|
8401
8648
|
2. Make targeted changes — do NOT rewrite code from scratch.
|
|
8402
8649
|
3. If a reviewer comment is unclear, make your best judgment and note your interpretation.
|
|
8403
8650
|
4. Push changes to the same branch — do NOT create a new PR.
|
|
8404
|
-
5. If you learned any reusable lessons from this feedback (non-obvious constraints, architectural patterns),
|
|
8651
|
+
5. If you learned any reusable lessons from this feedback (non-obvious constraints, architectural patterns), record them in the appropriate category file in \`.locus/memory/\`.
|
|
8405
8652
|
6. When done, summarize what you changed in response to each comment.
|
|
8406
8653
|
</instructions>`;
|
|
8407
8654
|
}
|
|
8408
8655
|
function buildSkillsContext(projectRoot) {
|
|
8409
|
-
const skillsDir =
|
|
8410
|
-
if (!
|
|
8656
|
+
const skillsDir = join17(projectRoot, CLAUDE_SKILLS_DIR);
|
|
8657
|
+
if (!existsSync17(skillsDir))
|
|
8411
8658
|
return null;
|
|
8412
8659
|
let dirs;
|
|
8413
8660
|
try {
|
|
@@ -8419,7 +8666,7 @@ function buildSkillsContext(projectRoot) {
|
|
|
8419
8666
|
return null;
|
|
8420
8667
|
const entries = [];
|
|
8421
8668
|
for (const dir of dirs) {
|
|
8422
|
-
const skillPath =
|
|
8669
|
+
const skillPath = join17(skillsDir, dir, "SKILL.md");
|
|
8423
8670
|
const content = readFileSafe(skillPath);
|
|
8424
8671
|
if (!content)
|
|
8425
8672
|
continue;
|
|
@@ -8456,14 +8703,17 @@ function parseFrontmatter(content) {
|
|
|
8456
8703
|
}
|
|
8457
8704
|
function readFileSafe(path) {
|
|
8458
8705
|
try {
|
|
8459
|
-
if (!
|
|
8706
|
+
if (!existsSync17(path))
|
|
8460
8707
|
return null;
|
|
8461
|
-
return
|
|
8708
|
+
return readFileSync12(path, "utf-8");
|
|
8462
8709
|
} catch {
|
|
8463
8710
|
return null;
|
|
8464
8711
|
}
|
|
8465
8712
|
}
|
|
8466
|
-
var
|
|
8713
|
+
var MEMORY_MAX_CHARS = 4000;
|
|
8714
|
+
var init_prompt_builder = __esm(() => {
|
|
8715
|
+
init_memory();
|
|
8716
|
+
});
|
|
8467
8717
|
|
|
8468
8718
|
// src/display/json-stream.ts
|
|
8469
8719
|
class JsonStream {
|
|
@@ -8555,6 +8805,180 @@ class JsonStream {
|
|
|
8555
8805
|
}
|
|
8556
8806
|
}
|
|
8557
8807
|
|
|
8808
|
+
// src/core/memory-capture.ts
|
|
8809
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
8810
|
+
function prepareTranscript(messages) {
|
|
8811
|
+
if (!messages || messages.length === 0)
|
|
8812
|
+
return "";
|
|
8813
|
+
const lines = [];
|
|
8814
|
+
for (const msg of messages) {
|
|
8815
|
+
const label = msg.role === "user" ? "User" : "Assistant";
|
|
8816
|
+
lines.push(`### ${label}
|
|
8817
|
+
${msg.content}`);
|
|
8818
|
+
}
|
|
8819
|
+
let transcript = lines.join(`
|
|
8820
|
+
|
|
8821
|
+
`);
|
|
8822
|
+
if (transcript.length > TRANSCRIPT_MAX_CHARS) {
|
|
8823
|
+
transcript = `${transcript.slice(0, TRANSCRIPT_MAX_CHARS)}
|
|
8824
|
+
|
|
8825
|
+
...(truncated)`;
|
|
8826
|
+
}
|
|
8827
|
+
return transcript;
|
|
8828
|
+
}
|
|
8829
|
+
function buildExtractionPrompt(transcript, existingMemory) {
|
|
8830
|
+
const categoryList = Object.entries(MEMORY_CATEGORIES).map(([key, meta]) => `- "${key}": ${meta.title} — ${meta.description}`).join(`
|
|
8831
|
+
`);
|
|
8832
|
+
return `You are a memory extraction assistant. Extract project-level reusable lessons from the following session transcript.
|
|
8833
|
+
|
|
8834
|
+
## Valid Categories
|
|
8835
|
+
|
|
8836
|
+
${categoryList}
|
|
8837
|
+
|
|
8838
|
+
## Existing Memory (for deduplication)
|
|
8839
|
+
|
|
8840
|
+
Do NOT extract entries that duplicate or closely overlap with these existing entries:
|
|
8841
|
+
|
|
8842
|
+
<existing-memory>
|
|
8843
|
+
${existingMemory || "(none)"}
|
|
8844
|
+
</existing-memory>
|
|
8845
|
+
|
|
8846
|
+
## Quality Bar
|
|
8847
|
+
|
|
8848
|
+
Only extract entries that would help a new agent on a future task. Skip:
|
|
8849
|
+
- Session-specific details or in-progress work
|
|
8850
|
+
- One-time fixes or trivial observations
|
|
8851
|
+
- Speculative or unverified conclusions
|
|
8852
|
+
- Anything that duplicates existing memory entries above
|
|
8853
|
+
|
|
8854
|
+
## Output Format
|
|
8855
|
+
|
|
8856
|
+
Respond with ONLY a JSON array. No markdown fencing, no explanation. Example:
|
|
8857
|
+
[{"category": "architecture", "text": "SDK types are shared via @locusai/shared package"}]
|
|
8858
|
+
|
|
8859
|
+
If there are no extractable lessons, respond with: []
|
|
8860
|
+
|
|
8861
|
+
## Session Transcript
|
|
8862
|
+
|
|
8863
|
+
${transcript}`;
|
|
8864
|
+
}
|
|
8865
|
+
function callExtractionAI(prompt, model) {
|
|
8866
|
+
return new Promise((resolve2) => {
|
|
8867
|
+
const args = [
|
|
8868
|
+
"--print",
|
|
8869
|
+
"--dangerously-skip-permissions",
|
|
8870
|
+
"--no-session-persistence"
|
|
8871
|
+
];
|
|
8872
|
+
if (model) {
|
|
8873
|
+
args.push("--model", model);
|
|
8874
|
+
}
|
|
8875
|
+
const env = { ...process.env };
|
|
8876
|
+
delete env.CLAUDECODE;
|
|
8877
|
+
delete env.CLAUDE_CODE;
|
|
8878
|
+
const proc = spawn6("claude", args, {
|
|
8879
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
8880
|
+
env
|
|
8881
|
+
});
|
|
8882
|
+
let output = "";
|
|
8883
|
+
let errorOutput = "";
|
|
8884
|
+
proc.stdout?.on("data", (chunk) => {
|
|
8885
|
+
output += chunk.toString();
|
|
8886
|
+
});
|
|
8887
|
+
proc.stderr?.on("data", (chunk) => {
|
|
8888
|
+
errorOutput += chunk.toString();
|
|
8889
|
+
});
|
|
8890
|
+
proc.on("close", (code) => {
|
|
8891
|
+
if (code === 0) {
|
|
8892
|
+
resolve2({ success: true, output });
|
|
8893
|
+
} else {
|
|
8894
|
+
resolve2({
|
|
8895
|
+
success: false,
|
|
8896
|
+
output,
|
|
8897
|
+
error: errorOutput || `claude exited with code ${code}`
|
|
8898
|
+
});
|
|
8899
|
+
}
|
|
8900
|
+
});
|
|
8901
|
+
proc.on("error", (err) => {
|
|
8902
|
+
resolve2({
|
|
8903
|
+
success: false,
|
|
8904
|
+
output: "",
|
|
8905
|
+
error: `Failed to spawn claude: ${err.message}`
|
|
8906
|
+
});
|
|
8907
|
+
});
|
|
8908
|
+
proc.stdin?.write(prompt);
|
|
8909
|
+
proc.stdin?.end();
|
|
8910
|
+
});
|
|
8911
|
+
}
|
|
8912
|
+
function parseExtractionResponse(raw) {
|
|
8913
|
+
let text = raw.trim();
|
|
8914
|
+
const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
8915
|
+
if (fenceMatch) {
|
|
8916
|
+
text = fenceMatch[1].trim();
|
|
8917
|
+
}
|
|
8918
|
+
const arrayMatch = text.match(/\[[\s\S]*\]/);
|
|
8919
|
+
if (!arrayMatch)
|
|
8920
|
+
return [];
|
|
8921
|
+
const parsed = JSON.parse(arrayMatch[0]);
|
|
8922
|
+
if (!Array.isArray(parsed))
|
|
8923
|
+
return [];
|
|
8924
|
+
return parsed.filter((item) => typeof item === "object" && item !== null && typeof item.category === "string" && typeof item.text === "string");
|
|
8925
|
+
}
|
|
8926
|
+
async function captureMemoryFromSession(projectRoot, transcript, config) {
|
|
8927
|
+
const log = getLogger();
|
|
8928
|
+
try {
|
|
8929
|
+
if (!transcript.trim()) {
|
|
8930
|
+
return { captured: 0 };
|
|
8931
|
+
}
|
|
8932
|
+
const existingMemory = await readAllMemory(projectRoot);
|
|
8933
|
+
const prompt = buildExtractionPrompt(transcript, existingMemory);
|
|
8934
|
+
const result = await callExtractionAI(prompt, config.model);
|
|
8935
|
+
if (!result.success) {
|
|
8936
|
+
log.warn("Memory capture: AI call failed", { error: result.error });
|
|
8937
|
+
return { captured: 0 };
|
|
8938
|
+
}
|
|
8939
|
+
let entries;
|
|
8940
|
+
try {
|
|
8941
|
+
entries = parseExtractionResponse(result.output);
|
|
8942
|
+
} catch (e) {
|
|
8943
|
+
log.warn("Memory capture: failed to parse AI response", {
|
|
8944
|
+
error: e instanceof Error ? e.message : String(e)
|
|
8945
|
+
});
|
|
8946
|
+
return { captured: 0 };
|
|
8947
|
+
}
|
|
8948
|
+
if (entries.length === 0) {
|
|
8949
|
+
return { captured: 0 };
|
|
8950
|
+
}
|
|
8951
|
+
const validEntries = entries.filter((entry) => {
|
|
8952
|
+
if (!VALID_CATEGORIES.has(entry.category)) {
|
|
8953
|
+
log.debug("Memory capture: skipping invalid category", {
|
|
8954
|
+
category: entry.category
|
|
8955
|
+
});
|
|
8956
|
+
return false;
|
|
8957
|
+
}
|
|
8958
|
+
return true;
|
|
8959
|
+
});
|
|
8960
|
+
if (validEntries.length === 0) {
|
|
8961
|
+
return { captured: 0 };
|
|
8962
|
+
}
|
|
8963
|
+
await appendMemoryEntries(projectRoot, validEntries);
|
|
8964
|
+
log.debug("Memory capture: extracted entries", {
|
|
8965
|
+
count: validEntries.length
|
|
8966
|
+
});
|
|
8967
|
+
return { captured: validEntries.length };
|
|
8968
|
+
} catch (e) {
|
|
8969
|
+
log.warn("Memory capture: unexpected error", {
|
|
8970
|
+
error: e instanceof Error ? e.message : String(e)
|
|
8971
|
+
});
|
|
8972
|
+
return { captured: 0 };
|
|
8973
|
+
}
|
|
8974
|
+
}
|
|
8975
|
+
var TRANSCRIPT_MAX_CHARS = 32000, VALID_CATEGORIES;
|
|
8976
|
+
var init_memory_capture = __esm(() => {
|
|
8977
|
+
init_logger();
|
|
8978
|
+
init_memory();
|
|
8979
|
+
VALID_CATEGORIES = new Set(Object.keys(MEMORY_CATEGORIES));
|
|
8980
|
+
});
|
|
8981
|
+
|
|
8558
8982
|
// src/display/diff-renderer.ts
|
|
8559
8983
|
function renderDiff(diff, options = {}) {
|
|
8560
8984
|
const { maxLines, lineNumbers = true } = options;
|
|
@@ -8936,7 +9360,7 @@ var init_commands = __esm(() => {
|
|
|
8936
9360
|
|
|
8937
9361
|
// src/repl/completions.ts
|
|
8938
9362
|
import { readdirSync as readdirSync5 } from "node:fs";
|
|
8939
|
-
import { basename as basename2, dirname as dirname4, join as
|
|
9363
|
+
import { basename as basename2, dirname as dirname4, join as join18 } from "node:path";
|
|
8940
9364
|
|
|
8941
9365
|
class SlashCommandCompletion {
|
|
8942
9366
|
commands;
|
|
@@ -8991,7 +9415,7 @@ class FilePathCompletion {
|
|
|
8991
9415
|
}
|
|
8992
9416
|
findMatches(partial) {
|
|
8993
9417
|
try {
|
|
8994
|
-
const dir = partial.includes("/") ?
|
|
9418
|
+
const dir = partial.includes("/") ? join18(this.projectRoot, dirname4(partial)) : this.projectRoot;
|
|
8995
9419
|
const prefix = basename2(partial);
|
|
8996
9420
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
8997
9421
|
return entries.filter((e) => {
|
|
@@ -9027,14 +9451,14 @@ class CombinedCompletion {
|
|
|
9027
9451
|
var init_completions = () => {};
|
|
9028
9452
|
|
|
9029
9453
|
// src/repl/input-history.ts
|
|
9030
|
-
import { existsSync as
|
|
9031
|
-
import { dirname as dirname5, join as
|
|
9454
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
|
|
9455
|
+
import { dirname as dirname5, join as join19 } from "node:path";
|
|
9032
9456
|
|
|
9033
9457
|
class InputHistory {
|
|
9034
9458
|
entries = [];
|
|
9035
9459
|
filePath;
|
|
9036
9460
|
constructor(projectRoot) {
|
|
9037
|
-
this.filePath =
|
|
9461
|
+
this.filePath = join19(projectRoot, ".locus", "sessions", ".input-history");
|
|
9038
9462
|
this.load();
|
|
9039
9463
|
}
|
|
9040
9464
|
add(text) {
|
|
@@ -9073,9 +9497,9 @@ class InputHistory {
|
|
|
9073
9497
|
}
|
|
9074
9498
|
load() {
|
|
9075
9499
|
try {
|
|
9076
|
-
if (!
|
|
9500
|
+
if (!existsSync18(this.filePath))
|
|
9077
9501
|
return;
|
|
9078
|
-
const content =
|
|
9502
|
+
const content = readFileSync13(this.filePath, "utf-8");
|
|
9079
9503
|
this.entries = content.split(`
|
|
9080
9504
|
`).map((line) => this.unescape(line)).filter(Boolean);
|
|
9081
9505
|
} catch {}
|
|
@@ -9083,7 +9507,7 @@ class InputHistory {
|
|
|
9083
9507
|
save() {
|
|
9084
9508
|
try {
|
|
9085
9509
|
const dir = dirname5(this.filePath);
|
|
9086
|
-
if (!
|
|
9510
|
+
if (!existsSync18(dir)) {
|
|
9087
9511
|
mkdirSync12(dir, { recursive: true });
|
|
9088
9512
|
}
|
|
9089
9513
|
const content = this.entries.map((e) => this.escape(e)).join(`
|
|
@@ -9114,20 +9538,20 @@ var init_model_config = __esm(() => {
|
|
|
9114
9538
|
|
|
9115
9539
|
// src/repl/session-manager.ts
|
|
9116
9540
|
import {
|
|
9117
|
-
existsSync as
|
|
9541
|
+
existsSync as existsSync19,
|
|
9118
9542
|
mkdirSync as mkdirSync13,
|
|
9119
9543
|
readdirSync as readdirSync6,
|
|
9120
|
-
readFileSync as
|
|
9544
|
+
readFileSync as readFileSync14,
|
|
9121
9545
|
unlinkSync as unlinkSync3,
|
|
9122
9546
|
writeFileSync as writeFileSync10
|
|
9123
9547
|
} from "node:fs";
|
|
9124
|
-
import { basename as basename3, join as
|
|
9548
|
+
import { basename as basename3, join as join20 } from "node:path";
|
|
9125
9549
|
|
|
9126
9550
|
class SessionManager {
|
|
9127
9551
|
sessionsDir;
|
|
9128
9552
|
constructor(projectRoot) {
|
|
9129
|
-
this.sessionsDir =
|
|
9130
|
-
if (!
|
|
9553
|
+
this.sessionsDir = join20(projectRoot, ".locus", "sessions");
|
|
9554
|
+
if (!existsSync19(this.sessionsDir)) {
|
|
9131
9555
|
mkdirSync13(this.sessionsDir, { recursive: true });
|
|
9132
9556
|
}
|
|
9133
9557
|
}
|
|
@@ -9153,14 +9577,14 @@ class SessionManager {
|
|
|
9153
9577
|
}
|
|
9154
9578
|
isPersisted(sessionOrId) {
|
|
9155
9579
|
const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
|
|
9156
|
-
return
|
|
9580
|
+
return existsSync19(this.getSessionPath(sessionId));
|
|
9157
9581
|
}
|
|
9158
9582
|
load(idOrPrefix) {
|
|
9159
9583
|
const files = this.listSessionFiles();
|
|
9160
9584
|
const exactPath = this.getSessionPath(idOrPrefix);
|
|
9161
|
-
if (
|
|
9585
|
+
if (existsSync19(exactPath)) {
|
|
9162
9586
|
try {
|
|
9163
|
-
return JSON.parse(
|
|
9587
|
+
return JSON.parse(readFileSync14(exactPath, "utf-8"));
|
|
9164
9588
|
} catch {
|
|
9165
9589
|
return null;
|
|
9166
9590
|
}
|
|
@@ -9168,7 +9592,7 @@ class SessionManager {
|
|
|
9168
9592
|
const matches = files.filter((f) => basename3(f, ".json").startsWith(idOrPrefix));
|
|
9169
9593
|
if (matches.length === 1) {
|
|
9170
9594
|
try {
|
|
9171
|
-
return JSON.parse(
|
|
9595
|
+
return JSON.parse(readFileSync14(matches[0], "utf-8"));
|
|
9172
9596
|
} catch {
|
|
9173
9597
|
return null;
|
|
9174
9598
|
}
|
|
@@ -9193,7 +9617,7 @@ class SessionManager {
|
|
|
9193
9617
|
const sessions = [];
|
|
9194
9618
|
for (const file of files) {
|
|
9195
9619
|
try {
|
|
9196
|
-
const session = JSON.parse(
|
|
9620
|
+
const session = JSON.parse(readFileSync14(file, "utf-8"));
|
|
9197
9621
|
sessions.push({
|
|
9198
9622
|
id: session.id,
|
|
9199
9623
|
created: session.created,
|
|
@@ -9208,7 +9632,7 @@ class SessionManager {
|
|
|
9208
9632
|
}
|
|
9209
9633
|
delete(sessionId) {
|
|
9210
9634
|
const path = this.getSessionPath(sessionId);
|
|
9211
|
-
if (
|
|
9635
|
+
if (existsSync19(path)) {
|
|
9212
9636
|
unlinkSync3(path);
|
|
9213
9637
|
return true;
|
|
9214
9638
|
}
|
|
@@ -9220,7 +9644,7 @@ class SessionManager {
|
|
|
9220
9644
|
let pruned = 0;
|
|
9221
9645
|
const withStats = files.map((f) => {
|
|
9222
9646
|
try {
|
|
9223
|
-
const session = JSON.parse(
|
|
9647
|
+
const session = JSON.parse(readFileSync14(f, "utf-8"));
|
|
9224
9648
|
return { path: f, updated: new Date(session.updated).getTime() };
|
|
9225
9649
|
} catch {
|
|
9226
9650
|
return { path: f, updated: 0 };
|
|
@@ -9238,7 +9662,7 @@ class SessionManager {
|
|
|
9238
9662
|
const remaining = withStats.length - pruned;
|
|
9239
9663
|
if (remaining > MAX_SESSIONS) {
|
|
9240
9664
|
const toRemove = remaining - MAX_SESSIONS;
|
|
9241
|
-
const alive = withStats.filter((e) =>
|
|
9665
|
+
const alive = withStats.filter((e) => existsSync19(e.path));
|
|
9242
9666
|
for (let i = 0;i < toRemove && i < alive.length; i++) {
|
|
9243
9667
|
try {
|
|
9244
9668
|
unlinkSync3(alive[i].path);
|
|
@@ -9253,7 +9677,7 @@ class SessionManager {
|
|
|
9253
9677
|
}
|
|
9254
9678
|
listSessionFiles() {
|
|
9255
9679
|
try {
|
|
9256
|
-
return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) =>
|
|
9680
|
+
return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join20(this.sessionsDir, f));
|
|
9257
9681
|
} catch {
|
|
9258
9682
|
return [];
|
|
9259
9683
|
}
|
|
@@ -9262,7 +9686,7 @@ class SessionManager {
|
|
|
9262
9686
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
9263
9687
|
}
|
|
9264
9688
|
getSessionPath(sessionId) {
|
|
9265
|
-
return
|
|
9689
|
+
return join20(this.sessionsDir, `${sessionId}.json`);
|
|
9266
9690
|
}
|
|
9267
9691
|
}
|
|
9268
9692
|
var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
|
|
@@ -9272,12 +9696,12 @@ var init_session_manager = __esm(() => {
|
|
|
9272
9696
|
});
|
|
9273
9697
|
|
|
9274
9698
|
// src/repl/voice.ts
|
|
9275
|
-
import { execSync as execSync11, spawn as
|
|
9276
|
-
import { existsSync as
|
|
9699
|
+
import { execSync as execSync11, spawn as spawn7 } from "node:child_process";
|
|
9700
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync14, unlinkSync as unlinkSync4 } from "node:fs";
|
|
9277
9701
|
import { cpus, homedir as homedir4, platform, tmpdir as tmpdir5 } from "node:os";
|
|
9278
|
-
import { join as
|
|
9702
|
+
import { join as join21 } from "node:path";
|
|
9279
9703
|
function getWhisperModelPath() {
|
|
9280
|
-
return
|
|
9704
|
+
return join21(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
|
|
9281
9705
|
}
|
|
9282
9706
|
function commandExists(cmd) {
|
|
9283
9707
|
try {
|
|
@@ -9295,16 +9719,16 @@ function findWhisperBinary() {
|
|
|
9295
9719
|
return name;
|
|
9296
9720
|
}
|
|
9297
9721
|
for (const name of candidates) {
|
|
9298
|
-
const fullPath =
|
|
9299
|
-
if (
|
|
9722
|
+
const fullPath = join21(LOCUS_BIN_DIR, name);
|
|
9723
|
+
if (existsSync20(fullPath))
|
|
9300
9724
|
return fullPath;
|
|
9301
9725
|
}
|
|
9302
9726
|
if (platform() === "darwin") {
|
|
9303
9727
|
const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
9304
9728
|
for (const dir of brewDirs) {
|
|
9305
9729
|
for (const name of candidates) {
|
|
9306
|
-
const fullPath =
|
|
9307
|
-
if (
|
|
9730
|
+
const fullPath = join21(dir, name);
|
|
9731
|
+
if (existsSync20(fullPath))
|
|
9308
9732
|
return fullPath;
|
|
9309
9733
|
}
|
|
9310
9734
|
}
|
|
@@ -9319,11 +9743,11 @@ function findSoxRecBinary() {
|
|
|
9319
9743
|
if (platform() === "darwin") {
|
|
9320
9744
|
const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
9321
9745
|
for (const dir of brewDirs) {
|
|
9322
|
-
const recPath =
|
|
9323
|
-
if (
|
|
9746
|
+
const recPath = join21(dir, "rec");
|
|
9747
|
+
if (existsSync20(recPath))
|
|
9324
9748
|
return recPath;
|
|
9325
|
-
const soxPath =
|
|
9326
|
-
if (
|
|
9749
|
+
const soxPath = join21(dir, "sox");
|
|
9750
|
+
if (existsSync20(soxPath))
|
|
9327
9751
|
return soxPath;
|
|
9328
9752
|
}
|
|
9329
9753
|
}
|
|
@@ -9332,7 +9756,7 @@ function findSoxRecBinary() {
|
|
|
9332
9756
|
function checkDependencies() {
|
|
9333
9757
|
const soxBinary = findSoxRecBinary();
|
|
9334
9758
|
const whisperBinary = findWhisperBinary();
|
|
9335
|
-
const modelDownloaded =
|
|
9759
|
+
const modelDownloaded = existsSync20(getWhisperModelPath());
|
|
9336
9760
|
return {
|
|
9337
9761
|
sox: soxBinary !== null,
|
|
9338
9762
|
whisper: whisperBinary !== null,
|
|
@@ -9486,7 +9910,7 @@ function ensureBuildDeps(pm) {
|
|
|
9486
9910
|
}
|
|
9487
9911
|
function buildWhisperFromSource(pm) {
|
|
9488
9912
|
const out = process.stderr;
|
|
9489
|
-
const buildDir =
|
|
9913
|
+
const buildDir = join21(tmpdir5(), `locus-whisper-build-${process.pid}`);
|
|
9490
9914
|
if (!ensureBuildDeps(pm)) {
|
|
9491
9915
|
out.write(` ${red2("✗")} Could not install build tools (cmake, g++, git).
|
|
9492
9916
|
`);
|
|
@@ -9497,8 +9921,8 @@ function buildWhisperFromSource(pm) {
|
|
|
9497
9921
|
mkdirSync14(LOCUS_BIN_DIR, { recursive: true });
|
|
9498
9922
|
out.write(` ${dim2("Cloning whisper.cpp...")}
|
|
9499
9923
|
`);
|
|
9500
|
-
execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${
|
|
9501
|
-
const srcDir =
|
|
9924
|
+
execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join21(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
|
|
9925
|
+
const srcDir = join21(buildDir, "whisper.cpp");
|
|
9502
9926
|
const numCpus = cpus().length || 2;
|
|
9503
9927
|
out.write(` ${dim2("Building whisper.cpp (this may take a few minutes)...")}
|
|
9504
9928
|
`);
|
|
@@ -9512,13 +9936,13 @@ function buildWhisperFromSource(pm) {
|
|
|
9512
9936
|
stdio: ["pipe", "pipe", "pipe"],
|
|
9513
9937
|
timeout: 600000
|
|
9514
9938
|
});
|
|
9515
|
-
const destPath =
|
|
9939
|
+
const destPath = join21(LOCUS_BIN_DIR, "whisper-cli");
|
|
9516
9940
|
const binaryCandidates = [
|
|
9517
|
-
|
|
9518
|
-
|
|
9941
|
+
join21(srcDir, "build", "bin", "whisper-cli"),
|
|
9942
|
+
join21(srcDir, "build", "bin", "main")
|
|
9519
9943
|
];
|
|
9520
9944
|
for (const candidate of binaryCandidates) {
|
|
9521
|
-
if (
|
|
9945
|
+
if (existsSync20(candidate)) {
|
|
9522
9946
|
execSync11(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
|
|
9523
9947
|
stdio: "pipe"
|
|
9524
9948
|
});
|
|
@@ -9597,7 +10021,7 @@ ${bold2("Installing voice dependencies...")}
|
|
|
9597
10021
|
}
|
|
9598
10022
|
function downloadModel() {
|
|
9599
10023
|
const modelPath = getWhisperModelPath();
|
|
9600
|
-
if (
|
|
10024
|
+
if (existsSync20(modelPath))
|
|
9601
10025
|
return true;
|
|
9602
10026
|
mkdirSync14(WHISPER_MODELS_DIR, { recursive: true });
|
|
9603
10027
|
const url = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${WHISPER_MODEL === "base.en" ? "ggml-base.en.bin" : `ggml-${WHISPER_MODEL}.bin`}`;
|
|
@@ -9644,7 +10068,7 @@ class VoiceController {
|
|
|
9644
10068
|
onStateChange;
|
|
9645
10069
|
constructor(options) {
|
|
9646
10070
|
this.onStateChange = options.onStateChange;
|
|
9647
|
-
this.tempFile =
|
|
10071
|
+
this.tempFile = join21(tmpdir5(), `locus-voice-${process.pid}.wav`);
|
|
9648
10072
|
this.deps = checkDependencies();
|
|
9649
10073
|
}
|
|
9650
10074
|
getState() {
|
|
@@ -9680,7 +10104,7 @@ class VoiceController {
|
|
|
9680
10104
|
return false;
|
|
9681
10105
|
}
|
|
9682
10106
|
const spawnArgs = binary === "rec" ? args : ["-d", ...args];
|
|
9683
|
-
this.recordProcess =
|
|
10107
|
+
this.recordProcess = spawn7(binary, spawnArgs, {
|
|
9684
10108
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9685
10109
|
});
|
|
9686
10110
|
this.recordProcess.on("error", (err) => {
|
|
@@ -9712,7 +10136,7 @@ ${red2("✗")} Recording failed: ${err.message}\r
|
|
|
9712
10136
|
this.recordProcess = null;
|
|
9713
10137
|
this.setState("idle");
|
|
9714
10138
|
await sleep2(200);
|
|
9715
|
-
if (!
|
|
10139
|
+
if (!existsSync20(this.tempFile)) {
|
|
9716
10140
|
return null;
|
|
9717
10141
|
}
|
|
9718
10142
|
try {
|
|
@@ -9747,7 +10171,7 @@ ${red2("✗")} Recording failed: ${err.message}\r
|
|
|
9747
10171
|
"--language",
|
|
9748
10172
|
WHISPER_MODEL.endsWith(".en") ? "en" : "auto"
|
|
9749
10173
|
];
|
|
9750
|
-
const proc =
|
|
10174
|
+
const proc = spawn7(binary, args, {
|
|
9751
10175
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9752
10176
|
});
|
|
9753
10177
|
let stdout = "";
|
|
@@ -9803,8 +10227,8 @@ function sleep2(ms) {
|
|
|
9803
10227
|
var WHISPER_MODEL = "base.en", WHISPER_MODELS_DIR, LOCUS_BIN_DIR;
|
|
9804
10228
|
var init_voice = __esm(() => {
|
|
9805
10229
|
init_terminal();
|
|
9806
|
-
WHISPER_MODELS_DIR =
|
|
9807
|
-
LOCUS_BIN_DIR =
|
|
10230
|
+
WHISPER_MODELS_DIR = join21(homedir4(), ".locus", "whisper-models");
|
|
10231
|
+
LOCUS_BIN_DIR = join21(homedir4(), ".locus", "bin");
|
|
9808
10232
|
});
|
|
9809
10233
|
|
|
9810
10234
|
// src/repl/repl.ts
|
|
@@ -9842,6 +10266,16 @@ async function startRepl(options) {
|
|
|
9842
10266
|
}
|
|
9843
10267
|
if (options.prompt) {
|
|
9844
10268
|
await executeOneShotPrompt(options.prompt, session, sessionManager, options);
|
|
10269
|
+
if (session.messages.length >= 2) {
|
|
10270
|
+
const log = getLogger();
|
|
10271
|
+
const transcript = prepareTranscript(session.messages);
|
|
10272
|
+
captureMemoryFromSession(projectRoot, transcript, {
|
|
10273
|
+
model: config.ai?.model
|
|
10274
|
+
}).then((result) => {
|
|
10275
|
+
if (result.captured > 0)
|
|
10276
|
+
log.info(`Captured ${result.captured} memory entries`);
|
|
10277
|
+
}).catch(() => {});
|
|
10278
|
+
}
|
|
9845
10279
|
return;
|
|
9846
10280
|
}
|
|
9847
10281
|
if (!process.stdin.isTTY) {
|
|
@@ -9906,6 +10340,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
9906
10340
|
});
|
|
9907
10341
|
printWelcome(session);
|
|
9908
10342
|
let shouldExit = false;
|
|
10343
|
+
let wasInterrupted = false;
|
|
9909
10344
|
let currentProvider = inferProviderFromModel(config.ai.model) || config.ai.provider;
|
|
9910
10345
|
let currentModel = config.ai.model;
|
|
9911
10346
|
let verbose = true;
|
|
@@ -10021,6 +10456,7 @@ ${red2("✗")} ${msg}
|
|
|
10021
10456
|
break;
|
|
10022
10457
|
}
|
|
10023
10458
|
case "interrupt":
|
|
10459
|
+
wasInterrupted = true;
|
|
10024
10460
|
shouldExit = true;
|
|
10025
10461
|
break;
|
|
10026
10462
|
case "exit":
|
|
@@ -10031,6 +10467,16 @@ ${red2("✗")} ${msg}
|
|
|
10031
10467
|
}
|
|
10032
10468
|
}
|
|
10033
10469
|
voice.cancel();
|
|
10470
|
+
if (!wasInterrupted && session.messages.length >= 2) {
|
|
10471
|
+
const log = getLogger();
|
|
10472
|
+
const transcript = prepareTranscript(session.messages);
|
|
10473
|
+
captureMemoryFromSession(projectRoot, transcript, {
|
|
10474
|
+
model: config.ai?.model
|
|
10475
|
+
}).then((result) => {
|
|
10476
|
+
if (result.captured > 0)
|
|
10477
|
+
log.info(`Captured ${result.captured} memory entries`);
|
|
10478
|
+
}).catch(() => {});
|
|
10479
|
+
}
|
|
10034
10480
|
const shouldPersistOnExit = session.messages.length > 0 || sessionManager.isPersisted(session);
|
|
10035
10481
|
if (shouldPersistOnExit) {
|
|
10036
10482
|
sessionManager.save(session);
|
|
@@ -10107,6 +10553,8 @@ var init_repl = __esm(() => {
|
|
|
10107
10553
|
init_run_ai();
|
|
10108
10554
|
init_runner();
|
|
10109
10555
|
init_ai_models();
|
|
10556
|
+
init_logger();
|
|
10557
|
+
init_memory_capture();
|
|
10110
10558
|
init_prompt_builder();
|
|
10111
10559
|
init_sandbox();
|
|
10112
10560
|
init_terminal();
|
|
@@ -10316,8 +10764,8 @@ var init_exec = __esm(() => {
|
|
|
10316
10764
|
|
|
10317
10765
|
// src/core/submodule.ts
|
|
10318
10766
|
import { execSync as execSync13 } from "node:child_process";
|
|
10319
|
-
import { existsSync as
|
|
10320
|
-
import { join as
|
|
10767
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
10768
|
+
import { join as join22 } from "node:path";
|
|
10321
10769
|
function git2(args, cwd) {
|
|
10322
10770
|
return execSync13(`git ${args}`, {
|
|
10323
10771
|
cwd,
|
|
@@ -10333,7 +10781,7 @@ function gitSafe(args, cwd) {
|
|
|
10333
10781
|
}
|
|
10334
10782
|
}
|
|
10335
10783
|
function hasSubmodules(cwd) {
|
|
10336
|
-
return
|
|
10784
|
+
return existsSync21(join22(cwd, ".gitmodules"));
|
|
10337
10785
|
}
|
|
10338
10786
|
function listSubmodules(cwd) {
|
|
10339
10787
|
if (!hasSubmodules(cwd))
|
|
@@ -10353,7 +10801,7 @@ function listSubmodules(cwd) {
|
|
|
10353
10801
|
continue;
|
|
10354
10802
|
submodules.push({
|
|
10355
10803
|
path,
|
|
10356
|
-
absolutePath:
|
|
10804
|
+
absolutePath: join22(cwd, path),
|
|
10357
10805
|
dirty
|
|
10358
10806
|
});
|
|
10359
10807
|
}
|
|
@@ -10366,7 +10814,7 @@ function getDirtySubmodules(cwd) {
|
|
|
10366
10814
|
const submodules = listSubmodules(cwd);
|
|
10367
10815
|
const dirty = [];
|
|
10368
10816
|
for (const sub of submodules) {
|
|
10369
|
-
if (!
|
|
10817
|
+
if (!existsSync21(sub.absolutePath))
|
|
10370
10818
|
continue;
|
|
10371
10819
|
const status = gitSafe("status --porcelain", sub.absolutePath);
|
|
10372
10820
|
if (status && status.trim().length > 0) {
|
|
@@ -10453,7 +10901,7 @@ function pushSubmoduleBranches(cwd) {
|
|
|
10453
10901
|
const log = getLogger();
|
|
10454
10902
|
const submodules = listSubmodules(cwd);
|
|
10455
10903
|
for (const sub of submodules) {
|
|
10456
|
-
if (!
|
|
10904
|
+
if (!existsSync21(sub.absolutePath))
|
|
10457
10905
|
continue;
|
|
10458
10906
|
const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
|
|
10459
10907
|
if (!branch || branch === "HEAD")
|
|
@@ -10605,6 +11053,19 @@ ${summary}
|
|
|
10605
11053
|
Duration: ${timer.formatted()}${prNumber ? `
|
|
10606
11054
|
PR: #${prNumber}` : ""}`, { cwd: projectRoot });
|
|
10607
11055
|
} catch {}
|
|
11056
|
+
const transcript = prepareTranscript([
|
|
11057
|
+
{
|
|
11058
|
+
role: "user",
|
|
11059
|
+
content: `Issue #${issueNumber}: ${issue.title}
|
|
11060
|
+
|
|
11061
|
+
${issue.body}`
|
|
11062
|
+
},
|
|
11063
|
+
{ role: "assistant", content: output }
|
|
11064
|
+
]);
|
|
11065
|
+
captureMemoryFromSession(projectRoot, transcript, { model: config.ai?.model }).then((result) => {
|
|
11066
|
+
if (result.captured > 0)
|
|
11067
|
+
log.info(`Captured ${result.captured} memory entries`);
|
|
11068
|
+
}).catch(() => {});
|
|
10608
11069
|
return {
|
|
10609
11070
|
issueNumber,
|
|
10610
11071
|
success: true,
|
|
@@ -10786,6 +11247,7 @@ var init_agent = __esm(() => {
|
|
|
10786
11247
|
init_config();
|
|
10787
11248
|
init_github();
|
|
10788
11249
|
init_logger();
|
|
11250
|
+
init_memory_capture();
|
|
10789
11251
|
init_prompt_builder();
|
|
10790
11252
|
init_sandbox();
|
|
10791
11253
|
init_submodule();
|
|
@@ -10923,15 +11385,15 @@ var init_conflict = __esm(() => {
|
|
|
10923
11385
|
|
|
10924
11386
|
// src/core/run-state.ts
|
|
10925
11387
|
import {
|
|
10926
|
-
existsSync as
|
|
11388
|
+
existsSync as existsSync22,
|
|
10927
11389
|
mkdirSync as mkdirSync15,
|
|
10928
|
-
readFileSync as
|
|
11390
|
+
readFileSync as readFileSync15,
|
|
10929
11391
|
unlinkSync as unlinkSync5,
|
|
10930
11392
|
writeFileSync as writeFileSync11
|
|
10931
11393
|
} from "node:fs";
|
|
10932
|
-
import { dirname as dirname6, join as
|
|
11394
|
+
import { dirname as dirname6, join as join23 } from "node:path";
|
|
10933
11395
|
function getRunStateDir(projectRoot) {
|
|
10934
|
-
return
|
|
11396
|
+
return join23(projectRoot, ".locus", "run-state");
|
|
10935
11397
|
}
|
|
10936
11398
|
function sprintSlug(name) {
|
|
10937
11399
|
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -10939,16 +11401,16 @@ function sprintSlug(name) {
|
|
|
10939
11401
|
function getRunStatePath(projectRoot, sprintName) {
|
|
10940
11402
|
const dir = getRunStateDir(projectRoot);
|
|
10941
11403
|
if (sprintName) {
|
|
10942
|
-
return
|
|
11404
|
+
return join23(dir, `${sprintSlug(sprintName)}.json`);
|
|
10943
11405
|
}
|
|
10944
|
-
return
|
|
11406
|
+
return join23(dir, "_parallel.json");
|
|
10945
11407
|
}
|
|
10946
11408
|
function loadRunState(projectRoot, sprintName) {
|
|
10947
11409
|
const path = getRunStatePath(projectRoot, sprintName);
|
|
10948
|
-
if (!
|
|
11410
|
+
if (!existsSync22(path))
|
|
10949
11411
|
return null;
|
|
10950
11412
|
try {
|
|
10951
|
-
return JSON.parse(
|
|
11413
|
+
return JSON.parse(readFileSync15(path, "utf-8"));
|
|
10952
11414
|
} catch {
|
|
10953
11415
|
getLogger().warn("Corrupted run state file, ignoring");
|
|
10954
11416
|
return null;
|
|
@@ -10957,7 +11419,7 @@ function loadRunState(projectRoot, sprintName) {
|
|
|
10957
11419
|
function saveRunState(projectRoot, state) {
|
|
10958
11420
|
const path = getRunStatePath(projectRoot, state.sprint);
|
|
10959
11421
|
const dir = dirname6(path);
|
|
10960
|
-
if (!
|
|
11422
|
+
if (!existsSync22(dir)) {
|
|
10961
11423
|
mkdirSync15(dir, { recursive: true });
|
|
10962
11424
|
}
|
|
10963
11425
|
writeFileSync11(path, `${JSON.stringify(state, null, 2)}
|
|
@@ -10965,7 +11427,7 @@ function saveRunState(projectRoot, state) {
|
|
|
10965
11427
|
}
|
|
10966
11428
|
function clearRunState(projectRoot, sprintName) {
|
|
10967
11429
|
const path = getRunStatePath(projectRoot, sprintName);
|
|
10968
|
-
if (
|
|
11430
|
+
if (existsSync22(path)) {
|
|
10969
11431
|
unlinkSync5(path);
|
|
10970
11432
|
}
|
|
10971
11433
|
}
|
|
@@ -11123,23 +11585,23 @@ __export(exports_worktree, {
|
|
|
11123
11585
|
import { execSync as execSync16 } from "node:child_process";
|
|
11124
11586
|
import {
|
|
11125
11587
|
cpSync as cpSync2,
|
|
11126
|
-
existsSync as
|
|
11588
|
+
existsSync as existsSync23,
|
|
11127
11589
|
mkdirSync as mkdirSync16,
|
|
11128
11590
|
readdirSync as readdirSync7,
|
|
11129
11591
|
realpathSync,
|
|
11130
11592
|
statSync as statSync4
|
|
11131
11593
|
} from "node:fs";
|
|
11132
|
-
import { join as
|
|
11594
|
+
import { join as join24 } from "node:path";
|
|
11133
11595
|
function copyLocusDir(projectRoot, worktreePath) {
|
|
11134
|
-
const srcLocus =
|
|
11135
|
-
if (!
|
|
11596
|
+
const srcLocus = join24(projectRoot, ".locus");
|
|
11597
|
+
if (!existsSync23(srcLocus))
|
|
11136
11598
|
return;
|
|
11137
|
-
const destLocus =
|
|
11599
|
+
const destLocus = join24(worktreePath, ".locus");
|
|
11138
11600
|
mkdirSync16(destLocus, { recursive: true });
|
|
11139
11601
|
for (const entry of readdirSync7(srcLocus)) {
|
|
11140
11602
|
if (entry === "worktrees")
|
|
11141
11603
|
continue;
|
|
11142
|
-
cpSync2(
|
|
11604
|
+
cpSync2(join24(srcLocus, entry), join24(destLocus, entry), { recursive: true });
|
|
11143
11605
|
}
|
|
11144
11606
|
}
|
|
11145
11607
|
function git4(args, cwd) {
|
|
@@ -11157,13 +11619,13 @@ function gitSafe3(args, cwd) {
|
|
|
11157
11619
|
}
|
|
11158
11620
|
}
|
|
11159
11621
|
function getWorktreeDir(projectRoot) {
|
|
11160
|
-
return
|
|
11622
|
+
return join24(projectRoot, ".locus", "worktrees");
|
|
11161
11623
|
}
|
|
11162
11624
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
11163
|
-
return
|
|
11625
|
+
return join24(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
|
|
11164
11626
|
}
|
|
11165
11627
|
function getSprintWorktreePath(projectRoot, sprintSlug2) {
|
|
11166
|
-
return
|
|
11628
|
+
return join24(getWorktreeDir(projectRoot), `sprint-${sprintSlug2}`);
|
|
11167
11629
|
}
|
|
11168
11630
|
function generateBranchName(issueNumber) {
|
|
11169
11631
|
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
@@ -11176,7 +11638,7 @@ function createSprintWorktree(projectRoot, sprintName, baseBranch) {
|
|
|
11176
11638
|
const log = getLogger();
|
|
11177
11639
|
const slug = sprintSlug2(sprintName);
|
|
11178
11640
|
const worktreePath = getSprintWorktreePath(projectRoot, slug);
|
|
11179
|
-
if (
|
|
11641
|
+
if (existsSync23(worktreePath)) {
|
|
11180
11642
|
log.verbose(`Sprint worktree already exists for "${sprintName}"`);
|
|
11181
11643
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/sprint-${slug}`;
|
|
11182
11644
|
return { path: worktreePath, branch: existingBranch };
|
|
@@ -11197,7 +11659,7 @@ function removeSprintWorktree(projectRoot, sprintName) {
|
|
|
11197
11659
|
const log = getLogger();
|
|
11198
11660
|
const slug = sprintSlug2(sprintName);
|
|
11199
11661
|
const worktreePath = getSprintWorktreePath(projectRoot, slug);
|
|
11200
|
-
if (!
|
|
11662
|
+
if (!existsSync23(worktreePath)) {
|
|
11201
11663
|
log.verbose(`Sprint worktree for "${sprintName}" does not exist`);
|
|
11202
11664
|
return;
|
|
11203
11665
|
}
|
|
@@ -11227,7 +11689,7 @@ function getWorktreeBranch(worktreePath) {
|
|
|
11227
11689
|
function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
11228
11690
|
const log = getLogger();
|
|
11229
11691
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
11230
|
-
if (
|
|
11692
|
+
if (existsSync23(worktreePath)) {
|
|
11231
11693
|
log.verbose(`Worktree already exists for issue #${issueNumber}`);
|
|
11232
11694
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
|
|
11233
11695
|
return {
|
|
@@ -11256,7 +11718,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
11256
11718
|
function removeWorktree(projectRoot, issueNumber) {
|
|
11257
11719
|
const log = getLogger();
|
|
11258
11720
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
11259
|
-
if (!
|
|
11721
|
+
if (!existsSync23(worktreePath)) {
|
|
11260
11722
|
log.verbose(`Worktree for issue #${issueNumber} does not exist`);
|
|
11261
11723
|
return;
|
|
11262
11724
|
}
|
|
@@ -11275,7 +11737,7 @@ function removeWorktree(projectRoot, issueNumber) {
|
|
|
11275
11737
|
function listWorktrees(projectRoot) {
|
|
11276
11738
|
const log = getLogger();
|
|
11277
11739
|
const worktreeDir = getWorktreeDir(projectRoot);
|
|
11278
|
-
if (!
|
|
11740
|
+
if (!existsSync23(worktreeDir)) {
|
|
11279
11741
|
return [];
|
|
11280
11742
|
}
|
|
11281
11743
|
const entries = readdirSync7(worktreeDir).filter((entry) => entry.startsWith("issue-"));
|
|
@@ -11295,7 +11757,7 @@ function listWorktrees(projectRoot) {
|
|
|
11295
11757
|
if (!match)
|
|
11296
11758
|
continue;
|
|
11297
11759
|
const issueNumber = Number.parseInt(match[1], 10);
|
|
11298
|
-
const path =
|
|
11760
|
+
const path = join24(worktreeDir, entry);
|
|
11299
11761
|
const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
|
|
11300
11762
|
let resolvedPath;
|
|
11301
11763
|
try {
|
|
@@ -11335,7 +11797,7 @@ function cleanupStaleWorktrees(projectRoot) {
|
|
|
11335
11797
|
}
|
|
11336
11798
|
function pushWorktreeBranch(projectRoot, issueNumber) {
|
|
11337
11799
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
11338
|
-
if (!
|
|
11800
|
+
if (!existsSync23(worktreePath)) {
|
|
11339
11801
|
throw new Error(`Worktree for issue #${issueNumber} does not exist`);
|
|
11340
11802
|
}
|
|
11341
11803
|
const branch = getWorktreeBranch(worktreePath);
|
|
@@ -11347,18 +11809,18 @@ function pushWorktreeBranch(projectRoot, issueNumber) {
|
|
|
11347
11809
|
}
|
|
11348
11810
|
function hasWorktreeChanges(projectRoot, issueNumber) {
|
|
11349
11811
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
11350
|
-
if (!
|
|
11812
|
+
if (!existsSync23(worktreePath))
|
|
11351
11813
|
return false;
|
|
11352
11814
|
const status = gitSafe3("status --porcelain", worktreePath);
|
|
11353
11815
|
return status !== null && status.trim().length > 0;
|
|
11354
11816
|
}
|
|
11355
11817
|
function getWorktreeAge(projectRoot, issueNumber) {
|
|
11356
11818
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
11357
|
-
if (!
|
|
11819
|
+
if (!existsSync23(worktreePath))
|
|
11358
11820
|
return 0;
|
|
11359
11821
|
try {
|
|
11360
|
-
const
|
|
11361
|
-
return Date.now() -
|
|
11822
|
+
const stat2 = statSync4(worktreePath);
|
|
11823
|
+
return Date.now() - stat2.ctimeMs;
|
|
11362
11824
|
} catch {
|
|
11363
11825
|
return 0;
|
|
11364
11826
|
}
|
|
@@ -11374,8 +11836,8 @@ __export(exports_run, {
|
|
|
11374
11836
|
runCommand: () => runCommand
|
|
11375
11837
|
});
|
|
11376
11838
|
import { execSync as execSync17 } from "node:child_process";
|
|
11377
|
-
import { existsSync as
|
|
11378
|
-
import { join as
|
|
11839
|
+
import { existsSync as existsSync24 } from "node:fs";
|
|
11840
|
+
import { join as join25 } from "node:path";
|
|
11379
11841
|
function resolveExecutionContext(config, modelOverride) {
|
|
11380
11842
|
const model = modelOverride ?? config.ai.model;
|
|
11381
11843
|
const provider = inferProviderFromModel(model) ?? config.ai.provider;
|
|
@@ -11933,8 +12395,8 @@ async function handleResume(projectRoot, config, sandboxed, flags) {
|
|
|
11933
12395
|
const sprintsToResume = [];
|
|
11934
12396
|
try {
|
|
11935
12397
|
const { readdirSync: readdirSync8 } = await import("node:fs");
|
|
11936
|
-
const runStateDir =
|
|
11937
|
-
if (
|
|
12398
|
+
const runStateDir = join25(projectRoot, ".locus", "run-state");
|
|
12399
|
+
if (existsSync24(runStateDir)) {
|
|
11938
12400
|
const files = readdirSync8(runStateDir).filter((f) => f.endsWith(".json"));
|
|
11939
12401
|
for (const file of files) {
|
|
11940
12402
|
const sprintName = file === "_parallel.json" ? undefined : file.replace(/\.json$/, "");
|
|
@@ -11978,9 +12440,9 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}${state.sprint ? ` ($
|
|
|
11978
12440
|
let workDir = projectRoot;
|
|
11979
12441
|
if (state.type === "sprint" && state.sprint) {
|
|
11980
12442
|
const { getSprintWorktreePath: getSprintWorktreePath2, sprintSlug: sprintSlug3 } = await Promise.resolve().then(() => (init_worktree(), exports_worktree));
|
|
11981
|
-
const { existsSync:
|
|
12443
|
+
const { existsSync: existsSync25 } = await import("node:fs");
|
|
11982
12444
|
const wtPath = getSprintWorktreePath2(projectRoot, sprintSlug3(state.sprint));
|
|
11983
|
-
if (
|
|
12445
|
+
if (existsSync25(wtPath)) {
|
|
11984
12446
|
workDir = wtPath;
|
|
11985
12447
|
} else if (state.branch) {
|
|
11986
12448
|
try {
|
|
@@ -12210,8 +12672,8 @@ __export(exports_status, {
|
|
|
12210
12672
|
statusCommand: () => statusCommand
|
|
12211
12673
|
});
|
|
12212
12674
|
import { execSync as execSync18 } from "node:child_process";
|
|
12213
|
-
import { existsSync as
|
|
12214
|
-
import { dirname as dirname7, join as
|
|
12675
|
+
import { existsSync as existsSync25 } from "node:fs";
|
|
12676
|
+
import { dirname as dirname7, join as join26 } from "node:path";
|
|
12215
12677
|
async function statusCommand(projectRoot) {
|
|
12216
12678
|
const config = loadConfig(projectRoot);
|
|
12217
12679
|
const spinner = new Spinner;
|
|
@@ -12343,13 +12805,13 @@ ${drawBox(lines, { title: "Locus Status" })}
|
|
|
12343
12805
|
`);
|
|
12344
12806
|
}
|
|
12345
12807
|
function getPm2Bin() {
|
|
12346
|
-
const pkgsBin =
|
|
12347
|
-
if (
|
|
12808
|
+
const pkgsBin = join26(getPackagesDir(), "node_modules", ".bin", "pm2");
|
|
12809
|
+
if (existsSync25(pkgsBin))
|
|
12348
12810
|
return pkgsBin;
|
|
12349
12811
|
let dir = process.cwd();
|
|
12350
12812
|
while (dir !== dirname7(dir)) {
|
|
12351
|
-
const candidate =
|
|
12352
|
-
if (
|
|
12813
|
+
const candidate = join26(dir, "node_modules", ".bin", "pm2");
|
|
12814
|
+
if (existsSync25(candidate))
|
|
12353
12815
|
return candidate;
|
|
12354
12816
|
dir = dirname7(dir);
|
|
12355
12817
|
}
|
|
@@ -12545,13 +13007,13 @@ __export(exports_plan, {
|
|
|
12545
13007
|
parsePlanArgs: () => parsePlanArgs
|
|
12546
13008
|
});
|
|
12547
13009
|
import {
|
|
12548
|
-
existsSync as
|
|
13010
|
+
existsSync as existsSync26,
|
|
12549
13011
|
mkdirSync as mkdirSync17,
|
|
12550
13012
|
readdirSync as readdirSync8,
|
|
12551
|
-
readFileSync as
|
|
13013
|
+
readFileSync as readFileSync16,
|
|
12552
13014
|
writeFileSync as writeFileSync12
|
|
12553
13015
|
} from "node:fs";
|
|
12554
|
-
import { join as
|
|
13016
|
+
import { join as join27 } from "node:path";
|
|
12555
13017
|
function printHelp2() {
|
|
12556
13018
|
process.stderr.write(`
|
|
12557
13019
|
${bold2("locus plan")} — AI-powered sprint planning
|
|
@@ -12583,11 +13045,11 @@ function normalizeSprintName(name) {
|
|
|
12583
13045
|
return name.trim().toLowerCase();
|
|
12584
13046
|
}
|
|
12585
13047
|
function getPlansDir(projectRoot) {
|
|
12586
|
-
return
|
|
13048
|
+
return join27(projectRoot, ".locus", "plans");
|
|
12587
13049
|
}
|
|
12588
13050
|
function ensurePlansDir(projectRoot) {
|
|
12589
13051
|
const dir = getPlansDir(projectRoot);
|
|
12590
|
-
if (!
|
|
13052
|
+
if (!existsSync26(dir)) {
|
|
12591
13053
|
mkdirSync17(dir, { recursive: true });
|
|
12592
13054
|
}
|
|
12593
13055
|
return dir;
|
|
@@ -12597,14 +13059,14 @@ function generateId() {
|
|
|
12597
13059
|
}
|
|
12598
13060
|
function loadPlanFile(projectRoot, id) {
|
|
12599
13061
|
const dir = getPlansDir(projectRoot);
|
|
12600
|
-
if (!
|
|
13062
|
+
if (!existsSync26(dir))
|
|
12601
13063
|
return null;
|
|
12602
13064
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
|
|
12603
13065
|
const match = files.find((f) => f.startsWith(id));
|
|
12604
13066
|
if (!match)
|
|
12605
13067
|
return null;
|
|
12606
13068
|
try {
|
|
12607
|
-
const content =
|
|
13069
|
+
const content = readFileSync16(join27(dir, match), "utf-8");
|
|
12608
13070
|
return JSON.parse(content);
|
|
12609
13071
|
} catch {
|
|
12610
13072
|
return null;
|
|
@@ -12671,7 +13133,7 @@ async function planCommand(projectRoot, args, flags = {}) {
|
|
|
12671
13133
|
}
|
|
12672
13134
|
function handleListPlans(projectRoot) {
|
|
12673
13135
|
const dir = getPlansDir(projectRoot);
|
|
12674
|
-
if (!
|
|
13136
|
+
if (!existsSync26(dir)) {
|
|
12675
13137
|
process.stderr.write(`${dim2("No saved plans yet.")}
|
|
12676
13138
|
`);
|
|
12677
13139
|
return;
|
|
@@ -12689,7 +13151,7 @@ ${bold2("Saved Plans:")}
|
|
|
12689
13151
|
for (const file of files) {
|
|
12690
13152
|
const id = file.replace(".json", "");
|
|
12691
13153
|
try {
|
|
12692
|
-
const content =
|
|
13154
|
+
const content = readFileSync16(join27(dir, file), "utf-8");
|
|
12693
13155
|
const plan = JSON.parse(content);
|
|
12694
13156
|
const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
|
|
12695
13157
|
const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
|
|
@@ -12757,7 +13219,7 @@ async function handleRefinePlan(projectRoot, id, feedback, flags) {
|
|
|
12757
13219
|
return;
|
|
12758
13220
|
}
|
|
12759
13221
|
const config = loadConfig(projectRoot);
|
|
12760
|
-
const planPath =
|
|
13222
|
+
const planPath = join27(getPlansDir(projectRoot), `${plan.id}.json`);
|
|
12761
13223
|
const planPathRelative = `.locus/plans/${plan.id}.json`;
|
|
12762
13224
|
process.stderr.write(`
|
|
12763
13225
|
${bold2("Refining plan:")} ${cyan2(plan.directive)}
|
|
@@ -12796,7 +13258,7 @@ ${red2("✗")} Refinement failed: ${aiResult.error}
|
|
|
12796
13258
|
`);
|
|
12797
13259
|
return;
|
|
12798
13260
|
}
|
|
12799
|
-
if (!
|
|
13261
|
+
if (!existsSync26(planPath)) {
|
|
12800
13262
|
process.stderr.write(`
|
|
12801
13263
|
${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
|
|
12802
13264
|
`);
|
|
@@ -12804,7 +13266,7 @@ ${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
|
|
|
12804
13266
|
}
|
|
12805
13267
|
let updatedPlan;
|
|
12806
13268
|
try {
|
|
12807
|
-
const content =
|
|
13269
|
+
const content = readFileSync16(planPath, "utf-8");
|
|
12808
13270
|
updatedPlan = JSON.parse(content);
|
|
12809
13271
|
} catch {
|
|
12810
13272
|
process.stderr.write(`
|
|
@@ -12894,7 +13356,7 @@ ${bold2("Approving plan:")}
|
|
|
12894
13356
|
async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
|
|
12895
13357
|
const id = generateId();
|
|
12896
13358
|
const plansDir = ensurePlansDir(projectRoot);
|
|
12897
|
-
const planPath =
|
|
13359
|
+
const planPath = join27(plansDir, `${id}.json`);
|
|
12898
13360
|
const planPathRelative = `.locus/plans/${id}.json`;
|
|
12899
13361
|
const displayDirective = directive;
|
|
12900
13362
|
process.stderr.write(`
|
|
@@ -12937,7 +13399,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
|
|
|
12937
13399
|
`);
|
|
12938
13400
|
return;
|
|
12939
13401
|
}
|
|
12940
|
-
if (!
|
|
13402
|
+
if (!existsSync26(planPath)) {
|
|
12941
13403
|
process.stderr.write(`
|
|
12942
13404
|
${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
|
|
12943
13405
|
`);
|
|
@@ -12947,7 +13409,7 @@ ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
|
|
|
12947
13409
|
}
|
|
12948
13410
|
let plan;
|
|
12949
13411
|
try {
|
|
12950
|
-
const content =
|
|
13412
|
+
const content = readFileSync16(planPath, "utf-8");
|
|
12951
13413
|
plan = JSON.parse(content);
|
|
12952
13414
|
} catch {
|
|
12953
13415
|
process.stderr.write(`
|
|
@@ -13109,6 +13571,18 @@ ${bold2("Suggested Order:")}
|
|
|
13109
13571
|
`);
|
|
13110
13572
|
}
|
|
13111
13573
|
}
|
|
13574
|
+
function loadPastMemory(projectRoot) {
|
|
13575
|
+
const memoryDir = getMemoryDir(projectRoot);
|
|
13576
|
+
if (existsSync26(memoryDir)) {
|
|
13577
|
+
const content = readAllMemorySync(projectRoot);
|
|
13578
|
+
if (content.trim()) {
|
|
13579
|
+
return content.length > MEMORY_MAX_CHARS2 ? `${content.slice(0, MEMORY_MAX_CHARS2)}
|
|
13580
|
+
|
|
13581
|
+
...(truncated)` : content;
|
|
13582
|
+
}
|
|
13583
|
+
}
|
|
13584
|
+
return "";
|
|
13585
|
+
}
|
|
13112
13586
|
function buildPlanningPrompt(projectRoot, config, directive, sprintName, id, planPathRelative) {
|
|
13113
13587
|
const parts = [];
|
|
13114
13588
|
parts.push(`<role>
|
|
@@ -13119,18 +13593,17 @@ ${directive}${sprintName ? `
|
|
|
13119
13593
|
|
|
13120
13594
|
**Sprint:** ${sprintName}` : ""}
|
|
13121
13595
|
</directive>`);
|
|
13122
|
-
const locusPath =
|
|
13123
|
-
if (
|
|
13124
|
-
const content =
|
|
13596
|
+
const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
|
|
13597
|
+
if (existsSync26(locusPath)) {
|
|
13598
|
+
const content = readFileSync16(locusPath, "utf-8");
|
|
13125
13599
|
parts.push(`<project-context>
|
|
13126
13600
|
${content.slice(0, 3000)}
|
|
13127
13601
|
</project-context>`);
|
|
13128
13602
|
}
|
|
13129
|
-
const
|
|
13130
|
-
if (
|
|
13131
|
-
const content = readFileSync15(learningsPath, "utf-8");
|
|
13603
|
+
const memoryContent = loadPastMemory(projectRoot);
|
|
13604
|
+
if (memoryContent) {
|
|
13132
13605
|
parts.push(`<past-learnings>
|
|
13133
|
-
${
|
|
13606
|
+
${memoryContent}
|
|
13134
13607
|
</past-learnings>`);
|
|
13135
13608
|
}
|
|
13136
13609
|
parts.push(`<task>
|
|
@@ -13180,9 +13653,9 @@ ${JSON.stringify(plan, null, 2)}
|
|
|
13180
13653
|
parts.push(`<feedback>
|
|
13181
13654
|
${feedback}
|
|
13182
13655
|
</feedback>`);
|
|
13183
|
-
const locusPath =
|
|
13184
|
-
if (
|
|
13185
|
-
const content =
|
|
13656
|
+
const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
|
|
13657
|
+
if (existsSync26(locusPath)) {
|
|
13658
|
+
const content = readFileSync16(locusPath, "utf-8");
|
|
13186
13659
|
parts.push(`<project-context>
|
|
13187
13660
|
${content.slice(0, 3000)}
|
|
13188
13661
|
</project-context>`);
|
|
@@ -13350,10 +13823,12 @@ ${green("✓")} Created ${planned.length} issues.${milestoneTitle ? ` Sprint: ${
|
|
|
13350
13823
|
|
|
13351
13824
|
`);
|
|
13352
13825
|
}
|
|
13826
|
+
var MEMORY_MAX_CHARS2 = 2000;
|
|
13353
13827
|
var init_plan = __esm(() => {
|
|
13354
13828
|
init_run_ai();
|
|
13355
13829
|
init_config();
|
|
13356
13830
|
init_github();
|
|
13831
|
+
init_memory();
|
|
13357
13832
|
init_sandbox();
|
|
13358
13833
|
init_terminal();
|
|
13359
13834
|
});
|
|
@@ -13364,8 +13839,8 @@ __export(exports_review, {
|
|
|
13364
13839
|
reviewCommand: () => reviewCommand
|
|
13365
13840
|
});
|
|
13366
13841
|
import { execFileSync as execFileSync2, execSync as execSync19 } from "node:child_process";
|
|
13367
|
-
import { existsSync as
|
|
13368
|
-
import { join as
|
|
13842
|
+
import { existsSync as existsSync27, readFileSync as readFileSync17 } from "node:fs";
|
|
13843
|
+
import { join as join28 } from "node:path";
|
|
13369
13844
|
function printHelp3() {
|
|
13370
13845
|
process.stderr.write(`
|
|
13371
13846
|
${bold2("locus review")} — AI-powered code review
|
|
@@ -13534,9 +14009,9 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
|
|
|
13534
14009
|
parts.push(`<role>
|
|
13535
14010
|
You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
|
|
13536
14011
|
</role>`);
|
|
13537
|
-
const locusPath =
|
|
13538
|
-
if (
|
|
13539
|
-
const content =
|
|
14012
|
+
const locusPath = join28(projectRoot, ".locus", "LOCUS.md");
|
|
14013
|
+
if (existsSync27(locusPath)) {
|
|
14014
|
+
const content = readFileSync17(locusPath, "utf-8");
|
|
13540
14015
|
parts.push(`<project-context>
|
|
13541
14016
|
${content.slice(0, 2000)}
|
|
13542
14017
|
</project-context>`);
|
|
@@ -13851,14 +14326,14 @@ __export(exports_discuss, {
|
|
|
13851
14326
|
discussCommand: () => discussCommand
|
|
13852
14327
|
});
|
|
13853
14328
|
import {
|
|
13854
|
-
existsSync as
|
|
14329
|
+
existsSync as existsSync28,
|
|
13855
14330
|
mkdirSync as mkdirSync18,
|
|
13856
14331
|
readdirSync as readdirSync9,
|
|
13857
|
-
readFileSync as
|
|
14332
|
+
readFileSync as readFileSync18,
|
|
13858
14333
|
unlinkSync as unlinkSync6,
|
|
13859
14334
|
writeFileSync as writeFileSync13
|
|
13860
14335
|
} from "node:fs";
|
|
13861
|
-
import { join as
|
|
14336
|
+
import { join as join29 } from "node:path";
|
|
13862
14337
|
function printHelp5() {
|
|
13863
14338
|
process.stderr.write(`
|
|
13864
14339
|
${bold2("locus discuss")} — AI-powered architectural discussions
|
|
@@ -13880,11 +14355,11 @@ ${bold2("Examples:")}
|
|
|
13880
14355
|
`);
|
|
13881
14356
|
}
|
|
13882
14357
|
function getDiscussionsDir(projectRoot) {
|
|
13883
|
-
return
|
|
14358
|
+
return join29(projectRoot, ".locus", "discussions");
|
|
13884
14359
|
}
|
|
13885
14360
|
function ensureDiscussionsDir(projectRoot) {
|
|
13886
14361
|
const dir = getDiscussionsDir(projectRoot);
|
|
13887
|
-
if (!
|
|
14362
|
+
if (!existsSync28(dir)) {
|
|
13888
14363
|
mkdirSync18(dir, { recursive: true });
|
|
13889
14364
|
}
|
|
13890
14365
|
return dir;
|
|
@@ -13919,7 +14394,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
|
|
|
13919
14394
|
}
|
|
13920
14395
|
function listDiscussions(projectRoot) {
|
|
13921
14396
|
const dir = getDiscussionsDir(projectRoot);
|
|
13922
|
-
if (!
|
|
14397
|
+
if (!existsSync28(dir)) {
|
|
13923
14398
|
process.stderr.write(`${dim2("No discussions yet.")}
|
|
13924
14399
|
`);
|
|
13925
14400
|
return;
|
|
@@ -13936,7 +14411,7 @@ ${bold2("Discussions:")}
|
|
|
13936
14411
|
`);
|
|
13937
14412
|
for (const file of files) {
|
|
13938
14413
|
const id = file.replace(".md", "");
|
|
13939
|
-
const content =
|
|
14414
|
+
const content = readFileSync18(join29(dir, file), "utf-8");
|
|
13940
14415
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
13941
14416
|
const title = titleMatch ? titleMatch[1] : id;
|
|
13942
14417
|
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
@@ -13954,7 +14429,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
13954
14429
|
return;
|
|
13955
14430
|
}
|
|
13956
14431
|
const dir = getDiscussionsDir(projectRoot);
|
|
13957
|
-
if (!
|
|
14432
|
+
if (!existsSync28(dir)) {
|
|
13958
14433
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
13959
14434
|
`);
|
|
13960
14435
|
return;
|
|
@@ -13966,7 +14441,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
13966
14441
|
`);
|
|
13967
14442
|
return;
|
|
13968
14443
|
}
|
|
13969
|
-
const content =
|
|
14444
|
+
const content = readFileSync18(join29(dir, match), "utf-8");
|
|
13970
14445
|
process.stdout.write(`${content}
|
|
13971
14446
|
`);
|
|
13972
14447
|
}
|
|
@@ -13977,7 +14452,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
13977
14452
|
return;
|
|
13978
14453
|
}
|
|
13979
14454
|
const dir = getDiscussionsDir(projectRoot);
|
|
13980
|
-
if (!
|
|
14455
|
+
if (!existsSync28(dir)) {
|
|
13981
14456
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
13982
14457
|
`);
|
|
13983
14458
|
return;
|
|
@@ -13989,7 +14464,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
13989
14464
|
`);
|
|
13990
14465
|
return;
|
|
13991
14466
|
}
|
|
13992
|
-
unlinkSync6(
|
|
14467
|
+
unlinkSync6(join29(dir, match));
|
|
13993
14468
|
process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
|
|
13994
14469
|
`);
|
|
13995
14470
|
}
|
|
@@ -14002,7 +14477,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
14002
14477
|
return;
|
|
14003
14478
|
}
|
|
14004
14479
|
const dir = getDiscussionsDir(projectRoot);
|
|
14005
|
-
if (!
|
|
14480
|
+
if (!existsSync28(dir)) {
|
|
14006
14481
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
14007
14482
|
`);
|
|
14008
14483
|
return;
|
|
@@ -14014,7 +14489,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
14014
14489
|
`);
|
|
14015
14490
|
return;
|
|
14016
14491
|
}
|
|
14017
|
-
const content =
|
|
14492
|
+
const content = readFileSync18(join29(dir, match), "utf-8");
|
|
14018
14493
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
14019
14494
|
const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
|
|
14020
14495
|
await planCommand(projectRoot, [
|
|
@@ -14140,7 +14615,7 @@ ${turn.content}`;
|
|
|
14140
14615
|
...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
|
|
14141
14616
|
].join(`
|
|
14142
14617
|
`);
|
|
14143
|
-
writeFileSync13(
|
|
14618
|
+
writeFileSync13(join29(dir, `${id}.md`), markdown, "utf-8");
|
|
14144
14619
|
process.stderr.write(`
|
|
14145
14620
|
${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
|
|
14146
14621
|
`);
|
|
@@ -14150,23 +14625,34 @@ ${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
|
|
|
14150
14625
|
|
|
14151
14626
|
`);
|
|
14152
14627
|
}
|
|
14628
|
+
function loadPastMemory2(projectRoot) {
|
|
14629
|
+
const memoryDir = getMemoryDir(projectRoot);
|
|
14630
|
+
if (existsSync28(memoryDir)) {
|
|
14631
|
+
const content = readAllMemorySync(projectRoot);
|
|
14632
|
+
if (content.trim()) {
|
|
14633
|
+
return content.length > MEMORY_MAX_CHARS3 ? `${content.slice(0, MEMORY_MAX_CHARS3)}
|
|
14634
|
+
|
|
14635
|
+
...(truncated)` : content;
|
|
14636
|
+
}
|
|
14637
|
+
}
|
|
14638
|
+
return "";
|
|
14639
|
+
}
|
|
14153
14640
|
function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFinal) {
|
|
14154
14641
|
const parts = [];
|
|
14155
14642
|
parts.push(`<role>
|
|
14156
14643
|
You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
|
|
14157
14644
|
</role>`);
|
|
14158
|
-
const locusPath =
|
|
14159
|
-
if (
|
|
14160
|
-
const content =
|
|
14645
|
+
const locusPath = join29(projectRoot, ".locus", "LOCUS.md");
|
|
14646
|
+
if (existsSync28(locusPath)) {
|
|
14647
|
+
const content = readFileSync18(locusPath, "utf-8");
|
|
14161
14648
|
parts.push(`<project-context>
|
|
14162
14649
|
${content.slice(0, 3000)}
|
|
14163
14650
|
</project-context>`);
|
|
14164
14651
|
}
|
|
14165
|
-
const
|
|
14166
|
-
if (
|
|
14167
|
-
const content = readFileSync17(learningsPath, "utf-8");
|
|
14652
|
+
const memoryContent = loadPastMemory2(projectRoot);
|
|
14653
|
+
if (memoryContent) {
|
|
14168
14654
|
parts.push(`<past-learnings>
|
|
14169
|
-
${
|
|
14655
|
+
${memoryContent}
|
|
14170
14656
|
</past-learnings>`);
|
|
14171
14657
|
}
|
|
14172
14658
|
parts.push(`<discussion-topic>
|
|
@@ -14215,10 +14701,11 @@ If you still need key information to give a good answer:
|
|
|
14215
14701
|
|
|
14216
14702
|
`);
|
|
14217
14703
|
}
|
|
14218
|
-
var MAX_DISCUSSION_ROUNDS = 5;
|
|
14704
|
+
var MAX_DISCUSSION_ROUNDS = 5, MEMORY_MAX_CHARS3 = 2000;
|
|
14219
14705
|
var init_discuss = __esm(() => {
|
|
14220
14706
|
init_run_ai();
|
|
14221
14707
|
init_config();
|
|
14708
|
+
init_memory();
|
|
14222
14709
|
init_sandbox();
|
|
14223
14710
|
init_progress();
|
|
14224
14711
|
init_terminal();
|
|
@@ -14235,8 +14722,8 @@ __export(exports_artifacts, {
|
|
|
14235
14722
|
formatDate: () => formatDate2,
|
|
14236
14723
|
artifactsCommand: () => artifactsCommand
|
|
14237
14724
|
});
|
|
14238
|
-
import { existsSync as
|
|
14239
|
-
import { join as
|
|
14725
|
+
import { existsSync as existsSync29, readdirSync as readdirSync10, readFileSync as readFileSync19, statSync as statSync5 } from "node:fs";
|
|
14726
|
+
import { join as join30 } from "node:path";
|
|
14240
14727
|
function printHelp6() {
|
|
14241
14728
|
process.stderr.write(`
|
|
14242
14729
|
${bold2("locus artifacts")} — View and manage AI-generated artifacts
|
|
@@ -14256,37 +14743,37 @@ ${dim2("Artifact names support partial matching.")}
|
|
|
14256
14743
|
`);
|
|
14257
14744
|
}
|
|
14258
14745
|
function getArtifactsDir(projectRoot) {
|
|
14259
|
-
return
|
|
14746
|
+
return join30(projectRoot, ".locus", "artifacts");
|
|
14260
14747
|
}
|
|
14261
14748
|
function listArtifacts(projectRoot) {
|
|
14262
14749
|
const dir = getArtifactsDir(projectRoot);
|
|
14263
|
-
if (!
|
|
14750
|
+
if (!existsSync29(dir))
|
|
14264
14751
|
return [];
|
|
14265
14752
|
return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
14266
|
-
const filePath =
|
|
14267
|
-
const
|
|
14753
|
+
const filePath = join30(dir, fileName);
|
|
14754
|
+
const stat2 = statSync5(filePath);
|
|
14268
14755
|
return {
|
|
14269
14756
|
name: fileName.replace(/\.md$/, ""),
|
|
14270
14757
|
fileName,
|
|
14271
|
-
createdAt:
|
|
14272
|
-
size:
|
|
14758
|
+
createdAt: stat2.birthtime,
|
|
14759
|
+
size: stat2.size
|
|
14273
14760
|
};
|
|
14274
14761
|
}).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
14275
14762
|
}
|
|
14276
14763
|
function readArtifact(projectRoot, name) {
|
|
14277
14764
|
const dir = getArtifactsDir(projectRoot);
|
|
14278
14765
|
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
14279
|
-
const filePath =
|
|
14280
|
-
if (!
|
|
14766
|
+
const filePath = join30(dir, fileName);
|
|
14767
|
+
if (!existsSync29(filePath))
|
|
14281
14768
|
return null;
|
|
14282
|
-
const
|
|
14769
|
+
const stat2 = statSync5(filePath);
|
|
14283
14770
|
return {
|
|
14284
|
-
content:
|
|
14771
|
+
content: readFileSync19(filePath, "utf-8"),
|
|
14285
14772
|
info: {
|
|
14286
14773
|
name: fileName.replace(/\.md$/, ""),
|
|
14287
14774
|
fileName,
|
|
14288
|
-
createdAt:
|
|
14289
|
-
size:
|
|
14775
|
+
createdAt: stat2.birthtime,
|
|
14776
|
+
size: stat2.size
|
|
14290
14777
|
}
|
|
14291
14778
|
};
|
|
14292
14779
|
}
|
|
@@ -14581,7 +15068,7 @@ Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
|
14581
15068
|
`);
|
|
14582
15069
|
}
|
|
14583
15070
|
}
|
|
14584
|
-
function buildCommitPrompt(diff,
|
|
15071
|
+
function buildCommitPrompt(diff, stat2, recentCommits) {
|
|
14585
15072
|
const maxDiffLength = 15000;
|
|
14586
15073
|
const truncatedDiff = diff.length > maxDiffLength ? `${diff.slice(0, maxDiffLength)}
|
|
14587
15074
|
|
|
@@ -14604,7 +15091,7 @@ ${recentCommits}
|
|
|
14604
15091
|
`;
|
|
14605
15092
|
}
|
|
14606
15093
|
prompt += `File summary:
|
|
14607
|
-
${
|
|
15094
|
+
${stat2}
|
|
14608
15095
|
|
|
14609
15096
|
Diff:
|
|
14610
15097
|
${truncatedDiff}`;
|
|
@@ -14638,10 +15125,10 @@ __export(exports_sandbox2, {
|
|
|
14638
15125
|
parseSandboxLogsArgs: () => parseSandboxLogsArgs,
|
|
14639
15126
|
parseSandboxInstallArgs: () => parseSandboxInstallArgs
|
|
14640
15127
|
});
|
|
14641
|
-
import { execSync as execSync22, spawn as
|
|
15128
|
+
import { execSync as execSync22, spawn as spawn8 } from "node:child_process";
|
|
14642
15129
|
import { createHash as createHash2 } from "node:crypto";
|
|
14643
|
-
import { existsSync as
|
|
14644
|
-
import { basename as basename4, join as
|
|
15130
|
+
import { existsSync as existsSync30, readFileSync as readFileSync20 } from "node:fs";
|
|
15131
|
+
import { basename as basename4, join as join31 } from "node:path";
|
|
14645
15132
|
import { createInterface as createInterface3 } from "node:readline";
|
|
14646
15133
|
function printSandboxHelp() {
|
|
14647
15134
|
process.stderr.write(`
|
|
@@ -14830,7 +15317,7 @@ async function handleAgentLogin(projectRoot, agent) {
|
|
|
14830
15317
|
process.stderr.write(`${dim2("Login and then exit when ready.")}
|
|
14831
15318
|
|
|
14832
15319
|
`);
|
|
14833
|
-
const child =
|
|
15320
|
+
const child = spawn8("docker", [
|
|
14834
15321
|
"sandbox",
|
|
14835
15322
|
"exec",
|
|
14836
15323
|
"--privileged",
|
|
@@ -15171,7 +15658,7 @@ async function handleLogs(projectRoot, args) {
|
|
|
15171
15658
|
}
|
|
15172
15659
|
function detectPackageManager2(projectRoot) {
|
|
15173
15660
|
try {
|
|
15174
|
-
const raw =
|
|
15661
|
+
const raw = readFileSync20(join31(projectRoot, "package.json"), "utf-8");
|
|
15175
15662
|
const pkgJson = JSON.parse(raw);
|
|
15176
15663
|
if (typeof pkgJson.packageManager === "string") {
|
|
15177
15664
|
const name = pkgJson.packageManager.split("@")[0];
|
|
@@ -15180,13 +15667,13 @@ function detectPackageManager2(projectRoot) {
|
|
|
15180
15667
|
}
|
|
15181
15668
|
}
|
|
15182
15669
|
} catch {}
|
|
15183
|
-
if (
|
|
15670
|
+
if (existsSync30(join31(projectRoot, "bun.lock")) || existsSync30(join31(projectRoot, "bun.lockb"))) {
|
|
15184
15671
|
return "bun";
|
|
15185
15672
|
}
|
|
15186
|
-
if (
|
|
15673
|
+
if (existsSync30(join31(projectRoot, "yarn.lock"))) {
|
|
15187
15674
|
return "yarn";
|
|
15188
15675
|
}
|
|
15189
|
-
if (
|
|
15676
|
+
if (existsSync30(join31(projectRoot, "pnpm-lock.yaml"))) {
|
|
15190
15677
|
return "pnpm";
|
|
15191
15678
|
}
|
|
15192
15679
|
return "npm";
|
|
@@ -15289,9 +15776,9 @@ Installing sandbox dependencies (${bold2(installCmd.join(" "))}) to container fi
|
|
|
15289
15776
|
${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
|
|
15290
15777
|
`);
|
|
15291
15778
|
}
|
|
15292
|
-
const setupScript =
|
|
15293
|
-
const containerSetupScript = containerWorkdir ?
|
|
15294
|
-
if (
|
|
15779
|
+
const setupScript = join31(projectRoot, ".locus", "sandbox-setup.sh");
|
|
15780
|
+
const containerSetupScript = containerWorkdir ? join31(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
|
|
15781
|
+
if (existsSync30(setupScript)) {
|
|
15295
15782
|
process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
|
|
15296
15783
|
`);
|
|
15297
15784
|
const hookOk = await runInteractiveCommand("docker", [
|
|
@@ -15373,7 +15860,7 @@ function getActiveProviderSandbox(projectRoot, provider) {
|
|
|
15373
15860
|
}
|
|
15374
15861
|
function runInteractiveCommand(command, args) {
|
|
15375
15862
|
return new Promise((resolve2) => {
|
|
15376
|
-
const child =
|
|
15863
|
+
const child = spawn8(command, args, { stdio: "inherit" });
|
|
15377
15864
|
child.on("close", (code) => resolve2(code === 0));
|
|
15378
15865
|
child.on("error", () => resolve2(false));
|
|
15379
15866
|
});
|
|
@@ -15458,23 +15945,295 @@ var init_sandbox2 = __esm(() => {
|
|
|
15458
15945
|
PROVIDERS = ["claude", "codex"];
|
|
15459
15946
|
});
|
|
15460
15947
|
|
|
15948
|
+
// src/commands/memory.ts
|
|
15949
|
+
var exports_memory = {};
|
|
15950
|
+
__export(exports_memory, {
|
|
15951
|
+
memoryCommand: () => memoryCommand
|
|
15952
|
+
});
|
|
15953
|
+
import { existsSync as existsSync31 } from "node:fs";
|
|
15954
|
+
import { writeFile as writeFile2 } from "node:fs/promises";
|
|
15955
|
+
import { join as join32 } from "node:path";
|
|
15956
|
+
import { createInterface as createInterface4 } from "node:readline";
|
|
15957
|
+
function printHelp7() {
|
|
15958
|
+
process.stderr.write(`
|
|
15959
|
+
${bold2("locus memory")} — Inspect, search, and manage structured memory
|
|
15960
|
+
|
|
15961
|
+
${bold2("Usage:")}
|
|
15962
|
+
locus memory list [--category <name>] List all memory entries
|
|
15963
|
+
locus memory search <query> Search entries by keyword
|
|
15964
|
+
locus memory stats Show per-category statistics
|
|
15965
|
+
locus memory reset [--confirm] Clear all entries (preserve headers)
|
|
15966
|
+
locus memory migrate Migrate legacy LEARNINGS.md → memory/
|
|
15967
|
+
|
|
15968
|
+
${bold2("Categories:")}
|
|
15969
|
+
architecture, conventions, decisions, preferences, debugging
|
|
15970
|
+
|
|
15971
|
+
${bold2("Examples:")}
|
|
15972
|
+
locus memory list ${dim2("# Show all entries")}
|
|
15973
|
+
locus memory list --category debugging ${dim2("# Show only debugging entries")}
|
|
15974
|
+
locus memory search "sandbox" ${dim2("# Search for 'sandbox'")}
|
|
15975
|
+
locus memory stats ${dim2("# Show entry counts and sizes")}
|
|
15976
|
+
locus memory reset --confirm ${dim2("# Clear all entries")}
|
|
15977
|
+
locus memory migrate ${dim2("# Migrate legacy LEARNINGS.md")}
|
|
15978
|
+
`);
|
|
15979
|
+
}
|
|
15980
|
+
async function memoryCommand(projectRoot, args) {
|
|
15981
|
+
const subcommand = args[0];
|
|
15982
|
+
if (!subcommand || subcommand === "help") {
|
|
15983
|
+
printHelp7();
|
|
15984
|
+
return;
|
|
15985
|
+
}
|
|
15986
|
+
const memoryDir = getMemoryDir(projectRoot);
|
|
15987
|
+
if (!existsSync31(memoryDir) && subcommand !== "migrate") {
|
|
15988
|
+
process.stderr.write(`${red2("✗")} Memory directory not found at ${dim2(".locus/memory/")}
|
|
15989
|
+
`);
|
|
15990
|
+
process.stderr.write(` Run ${bold2("locus init")} to create the memory directory.
|
|
15991
|
+
`);
|
|
15992
|
+
process.exit(1);
|
|
15993
|
+
}
|
|
15994
|
+
switch (subcommand) {
|
|
15995
|
+
case "list":
|
|
15996
|
+
await handleList(projectRoot, args.slice(1));
|
|
15997
|
+
break;
|
|
15998
|
+
case "search":
|
|
15999
|
+
await handleSearch(projectRoot, args.slice(1));
|
|
16000
|
+
break;
|
|
16001
|
+
case "stats":
|
|
16002
|
+
await handleStats(projectRoot);
|
|
16003
|
+
break;
|
|
16004
|
+
case "reset":
|
|
16005
|
+
await handleReset(projectRoot, args.slice(1));
|
|
16006
|
+
break;
|
|
16007
|
+
case "migrate":
|
|
16008
|
+
await handleMigrate(projectRoot);
|
|
16009
|
+
break;
|
|
16010
|
+
default:
|
|
16011
|
+
process.stderr.write(`${red2("✗")} Unknown subcommand: ${bold2(subcommand)}
|
|
16012
|
+
`);
|
|
16013
|
+
process.stderr.write(` Run ${bold2("locus memory help")} for available subcommands.
|
|
16014
|
+
`);
|
|
16015
|
+
process.exit(1);
|
|
16016
|
+
}
|
|
16017
|
+
}
|
|
16018
|
+
async function handleList(projectRoot, args) {
|
|
16019
|
+
let categoryFilter;
|
|
16020
|
+
const catIdx = args.indexOf("--category");
|
|
16021
|
+
if (catIdx !== -1) {
|
|
16022
|
+
categoryFilter = args[catIdx + 1];
|
|
16023
|
+
if (!categoryFilter || !MEMORY_CATEGORIES[categoryFilter]) {
|
|
16024
|
+
const valid = Object.keys(MEMORY_CATEGORIES).join(", ");
|
|
16025
|
+
process.stderr.write(`${red2("✗")} Invalid category. Valid categories: ${valid}
|
|
16026
|
+
`);
|
|
16027
|
+
process.exit(1);
|
|
16028
|
+
}
|
|
16029
|
+
}
|
|
16030
|
+
const categories = categoryFilter ? [categoryFilter] : Object.keys(MEMORY_CATEGORIES);
|
|
16031
|
+
let totalEntries = 0;
|
|
16032
|
+
for (const category of categories) {
|
|
16033
|
+
const content = await readMemoryFile(projectRoot, category);
|
|
16034
|
+
const entries = parseEntries(content);
|
|
16035
|
+
if (entries.length === 0)
|
|
16036
|
+
continue;
|
|
16037
|
+
const meta = MEMORY_CATEGORIES[category];
|
|
16038
|
+
process.stderr.write(`
|
|
16039
|
+
${bold2(cyan2(meta.title))} ${dim2(`(${meta.description})`)}
|
|
16040
|
+
`);
|
|
16041
|
+
for (const entry of entries) {
|
|
16042
|
+
process.stderr.write(` ${entry}
|
|
16043
|
+
`);
|
|
16044
|
+
}
|
|
16045
|
+
totalEntries += entries.length;
|
|
16046
|
+
}
|
|
16047
|
+
if (totalEntries === 0) {
|
|
16048
|
+
process.stderr.write(`
|
|
16049
|
+
${dim2("No memory entries found.")}
|
|
16050
|
+
`);
|
|
16051
|
+
} else {
|
|
16052
|
+
process.stderr.write(`
|
|
16053
|
+
${dim2(`${totalEntries} entries total`)}
|
|
16054
|
+
`);
|
|
16055
|
+
}
|
|
16056
|
+
}
|
|
16057
|
+
async function handleSearch(projectRoot, args) {
|
|
16058
|
+
const query = args.join(" ").trim();
|
|
16059
|
+
if (!query) {
|
|
16060
|
+
process.stderr.write(`${red2("✗")} Usage: locus memory search <query>
|
|
16061
|
+
`);
|
|
16062
|
+
process.exit(1);
|
|
16063
|
+
}
|
|
16064
|
+
const queryLower = query.toLowerCase();
|
|
16065
|
+
let matchCount = 0;
|
|
16066
|
+
for (const [category, meta] of Object.entries(MEMORY_CATEGORIES)) {
|
|
16067
|
+
const content = await readMemoryFile(projectRoot, category);
|
|
16068
|
+
const entries = parseEntries(content);
|
|
16069
|
+
const matches = entries.filter((entry) => entry.toLowerCase().includes(queryLower));
|
|
16070
|
+
if (matches.length === 0)
|
|
16071
|
+
continue;
|
|
16072
|
+
process.stderr.write(`
|
|
16073
|
+
${bold2(cyan2(meta.title))}
|
|
16074
|
+
`);
|
|
16075
|
+
for (const entry of matches) {
|
|
16076
|
+
const highlighted = highlightMatch(entry, query);
|
|
16077
|
+
process.stderr.write(` ${highlighted}
|
|
16078
|
+
`);
|
|
16079
|
+
}
|
|
16080
|
+
matchCount += matches.length;
|
|
16081
|
+
}
|
|
16082
|
+
if (matchCount === 0) {
|
|
16083
|
+
process.stderr.write(`
|
|
16084
|
+
${dim2("No matches found for")} "${query}"
|
|
16085
|
+
`);
|
|
16086
|
+
} else {
|
|
16087
|
+
process.stderr.write(`
|
|
16088
|
+
${green(`${matchCount} match${matchCount === 1 ? "" : "es"}`)} found for "${query}"
|
|
16089
|
+
`);
|
|
16090
|
+
}
|
|
16091
|
+
}
|
|
16092
|
+
async function handleStats(projectRoot) {
|
|
16093
|
+
const stats = await getMemoryStats(projectRoot);
|
|
16094
|
+
process.stderr.write(`
|
|
16095
|
+
${bold2("Memory Statistics")}
|
|
16096
|
+
|
|
16097
|
+
`);
|
|
16098
|
+
let totalEntries = 0;
|
|
16099
|
+
let totalSize = 0;
|
|
16100
|
+
for (const [key, meta] of Object.entries(MEMORY_CATEGORIES)) {
|
|
16101
|
+
const s = stats[key];
|
|
16102
|
+
const countStr = String(s.count).padStart(3);
|
|
16103
|
+
const sizeStr = formatSize2(s.size).padStart(8);
|
|
16104
|
+
const dateStr = s.size > 0 ? dim2(s.lastModified.toLocaleDateString("en-US", {
|
|
16105
|
+
month: "short",
|
|
16106
|
+
day: "numeric",
|
|
16107
|
+
year: "numeric"
|
|
16108
|
+
})) : dim2("—");
|
|
16109
|
+
const icon = s.count > 0 ? green("●") : dim2("○");
|
|
16110
|
+
process.stderr.write(` ${icon} ${cyan2(meta.title.padEnd(14))} ${countStr} entries ${sizeStr} ${dateStr}
|
|
16111
|
+
`);
|
|
16112
|
+
totalEntries += s.count;
|
|
16113
|
+
totalSize += s.size;
|
|
16114
|
+
}
|
|
16115
|
+
process.stderr.write(`
|
|
16116
|
+
${bold2("Total:")} ${totalEntries} entries, ${formatSize2(totalSize)}
|
|
16117
|
+
`);
|
|
16118
|
+
process.stderr.write(`
|
|
16119
|
+
`);
|
|
16120
|
+
}
|
|
16121
|
+
async function handleReset(projectRoot, args) {
|
|
16122
|
+
const confirmed = args.includes("--confirm");
|
|
16123
|
+
if (!confirmed) {
|
|
16124
|
+
const rl = createInterface4({
|
|
16125
|
+
input: process.stdin,
|
|
16126
|
+
output: process.stderr
|
|
16127
|
+
});
|
|
16128
|
+
const answer = await new Promise((resolve2) => {
|
|
16129
|
+
rl.question(`${yellow2("⚠")} This will clear all memory entries. Continue? (y/N) `, (ans) => {
|
|
16130
|
+
rl.close();
|
|
16131
|
+
resolve2(ans.trim().toLowerCase());
|
|
16132
|
+
});
|
|
16133
|
+
});
|
|
16134
|
+
if (answer !== "y" && answer !== "yes") {
|
|
16135
|
+
process.stderr.write(`${dim2("Cancelled.")}
|
|
16136
|
+
`);
|
|
16137
|
+
return;
|
|
16138
|
+
}
|
|
16139
|
+
}
|
|
16140
|
+
const stats = await getMemoryStats(projectRoot);
|
|
16141
|
+
const memoryDir = getMemoryDir(projectRoot);
|
|
16142
|
+
let totalCleared = 0;
|
|
16143
|
+
for (const [key, meta] of Object.entries(MEMORY_CATEGORIES)) {
|
|
16144
|
+
const count = stats[key].count;
|
|
16145
|
+
const filePath = join32(memoryDir, meta.file);
|
|
16146
|
+
const header = `# ${meta.title}
|
|
16147
|
+
|
|
16148
|
+
${meta.description}
|
|
16149
|
+
|
|
16150
|
+
`;
|
|
16151
|
+
await writeFile2(filePath, header, "utf-8");
|
|
16152
|
+
if (count > 0) {
|
|
16153
|
+
process.stderr.write(` ${dim2("○")} ${meta.title}: cleared ${count} entries
|
|
16154
|
+
`);
|
|
16155
|
+
totalCleared += count;
|
|
16156
|
+
}
|
|
16157
|
+
}
|
|
16158
|
+
if (totalCleared > 0) {
|
|
16159
|
+
process.stderr.write(`
|
|
16160
|
+
${green("✓")} Cleared ${totalCleared} entries (file headers preserved)
|
|
16161
|
+
`);
|
|
16162
|
+
} else {
|
|
16163
|
+
process.stderr.write(`${dim2("No entries to clear.")}
|
|
16164
|
+
`);
|
|
16165
|
+
}
|
|
16166
|
+
}
|
|
16167
|
+
async function handleMigrate(projectRoot) {
|
|
16168
|
+
const learningsPath = join32(projectRoot, ".locus", "LEARNINGS.md");
|
|
16169
|
+
if (!existsSync31(learningsPath)) {
|
|
16170
|
+
process.stderr.write(`${dim2("○")} No LEARNINGS.md found — nothing to migrate.
|
|
16171
|
+
`);
|
|
16172
|
+
return;
|
|
16173
|
+
}
|
|
16174
|
+
process.stderr.write(`Migrating entries from LEARNINGS.md...
|
|
16175
|
+
`);
|
|
16176
|
+
const result = await migrateFromLearnings(projectRoot);
|
|
16177
|
+
if (result.migrated > 0) {
|
|
16178
|
+
process.stderr.write(`${green("✓")} Migrated ${result.migrated} entries to .locus/memory/
|
|
16179
|
+
`);
|
|
16180
|
+
}
|
|
16181
|
+
if (result.skipped > 0) {
|
|
16182
|
+
process.stderr.write(`${dim2("○")} Skipped ${result.skipped} duplicate entries
|
|
16183
|
+
`);
|
|
16184
|
+
}
|
|
16185
|
+
if (result.migrated > 0 || result.skipped > 0) {
|
|
16186
|
+
process.stderr.write(`${green("✓")} Removed legacy LEARNINGS.md
|
|
16187
|
+
`);
|
|
16188
|
+
}
|
|
16189
|
+
if (result.migrated === 0 && result.skipped === 0) {
|
|
16190
|
+
process.stderr.write(`${dim2("○")} No entries found in LEARNINGS.md to migrate.
|
|
16191
|
+
`);
|
|
16192
|
+
}
|
|
16193
|
+
}
|
|
16194
|
+
function parseEntries(content) {
|
|
16195
|
+
if (!content)
|
|
16196
|
+
return [];
|
|
16197
|
+
return content.split(`
|
|
16198
|
+
`).filter((line) => line.startsWith("- "));
|
|
16199
|
+
}
|
|
16200
|
+
function highlightMatch(text, query) {
|
|
16201
|
+
const regex = new RegExp(`(${escapeRegex(query)})`, "gi");
|
|
16202
|
+
return text.replace(regex, (match) => bold2(match));
|
|
16203
|
+
}
|
|
16204
|
+
function escapeRegex(str) {
|
|
16205
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16206
|
+
}
|
|
16207
|
+
function formatSize2(bytes) {
|
|
16208
|
+
if (bytes === 0)
|
|
16209
|
+
return "0 B";
|
|
16210
|
+
if (bytes < 1024)
|
|
16211
|
+
return `${bytes} B`;
|
|
16212
|
+
const kb = bytes / 1024;
|
|
16213
|
+
return `${kb.toFixed(1)} KB`;
|
|
16214
|
+
}
|
|
16215
|
+
var init_memory2 = __esm(() => {
|
|
16216
|
+
init_memory();
|
|
16217
|
+
init_terminal();
|
|
16218
|
+
});
|
|
16219
|
+
|
|
15461
16220
|
// src/cli.ts
|
|
15462
16221
|
init_config();
|
|
15463
16222
|
init_context();
|
|
15464
16223
|
init_logger();
|
|
15465
16224
|
init_rate_limiter();
|
|
15466
16225
|
init_terminal();
|
|
15467
|
-
import { existsSync as
|
|
15468
|
-
import { join as
|
|
16226
|
+
import { existsSync as existsSync32, readFileSync as readFileSync21 } from "node:fs";
|
|
16227
|
+
import { join as join33 } from "node:path";
|
|
15469
16228
|
import { fileURLToPath } from "node:url";
|
|
15470
16229
|
function getCliVersion() {
|
|
15471
16230
|
const fallbackVersion = "0.0.0";
|
|
15472
|
-
const packageJsonPath =
|
|
15473
|
-
if (!
|
|
16231
|
+
const packageJsonPath = join33(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
|
|
16232
|
+
if (!existsSync32(packageJsonPath)) {
|
|
15474
16233
|
return fallbackVersion;
|
|
15475
16234
|
}
|
|
15476
16235
|
try {
|
|
15477
|
-
const parsed = JSON.parse(
|
|
16236
|
+
const parsed = JSON.parse(readFileSync21(packageJsonPath, "utf-8"));
|
|
15478
16237
|
return parsed.version ?? fallbackVersion;
|
|
15479
16238
|
} catch {
|
|
15480
16239
|
return fallbackVersion;
|
|
@@ -15636,7 +16395,7 @@ function printLogo() {
|
|
|
15636
16395
|
`);
|
|
15637
16396
|
}
|
|
15638
16397
|
}
|
|
15639
|
-
function
|
|
16398
|
+
function printHelp8() {
|
|
15640
16399
|
printLogo();
|
|
15641
16400
|
process.stderr.write(`
|
|
15642
16401
|
|
|
@@ -15664,6 +16423,7 @@ ${bold2("Commands:")}
|
|
|
15664
16423
|
${cyan2("packages")} Manage installed packages (list, outdated)
|
|
15665
16424
|
${cyan2("pkg")} ${dim2("<name> [cmd]")} Run a command from an installed package
|
|
15666
16425
|
${cyan2("skills")} Discover and manage agent skills
|
|
16426
|
+
${cyan2("memory")} Inspect, search, and manage memory
|
|
15667
16427
|
${cyan2("sandbox")} Manage Docker sandbox lifecycle
|
|
15668
16428
|
${cyan2("upgrade")} Check for and install updates
|
|
15669
16429
|
|
|
@@ -15746,7 +16506,7 @@ async function main() {
|
|
|
15746
16506
|
process.exit(0);
|
|
15747
16507
|
}
|
|
15748
16508
|
if (parsed.flags.help && !parsed.command) {
|
|
15749
|
-
|
|
16509
|
+
printHelp8();
|
|
15750
16510
|
process.exit(0);
|
|
15751
16511
|
}
|
|
15752
16512
|
const command = resolveAlias(parsed.command);
|
|
@@ -15757,7 +16517,7 @@ async function main() {
|
|
|
15757
16517
|
try {
|
|
15758
16518
|
const root = getGitRoot(cwd);
|
|
15759
16519
|
if (isInitialized(root)) {
|
|
15760
|
-
logDir =
|
|
16520
|
+
logDir = join33(root, ".locus", "logs");
|
|
15761
16521
|
getRateLimiter(root);
|
|
15762
16522
|
}
|
|
15763
16523
|
} catch {}
|
|
@@ -15778,7 +16538,7 @@ async function main() {
|
|
|
15778
16538
|
printVersionNotice = startVersionCheck2(VERSION);
|
|
15779
16539
|
}
|
|
15780
16540
|
if (!command) {
|
|
15781
|
-
|
|
16541
|
+
printHelp8();
|
|
15782
16542
|
process.exit(0);
|
|
15783
16543
|
}
|
|
15784
16544
|
if (command === "init") {
|
|
@@ -15974,6 +16734,12 @@ async function main() {
|
|
|
15974
16734
|
await sandboxCommand2(projectRoot, sandboxArgs);
|
|
15975
16735
|
break;
|
|
15976
16736
|
}
|
|
16737
|
+
case "memory": {
|
|
16738
|
+
const { memoryCommand: memoryCommand2 } = await Promise.resolve().then(() => (init_memory2(), exports_memory));
|
|
16739
|
+
const memoryArgs = parsed.flags.help ? ["help"] : parsed.args;
|
|
16740
|
+
await memoryCommand2(projectRoot, memoryArgs);
|
|
16741
|
+
break;
|
|
16742
|
+
}
|
|
15977
16743
|
case "upgrade": {
|
|
15978
16744
|
const { upgradeCommand: upgradeCommand2 } = await Promise.resolve().then(() => (init_upgrade(), exports_upgrade));
|
|
15979
16745
|
await upgradeCommand2(projectRoot, parsed.args, {
|