@locusai/cli 0.25.6 → 0.26.1
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 +1341 -375
- 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(() => {
|
|
@@ -3880,6 +4101,80 @@ ${bold2("Skill Information")}
|
|
|
3880
4101
|
process.stderr.write(`
|
|
3881
4102
|
`);
|
|
3882
4103
|
}
|
|
4104
|
+
async function searchSkills(query, flags) {
|
|
4105
|
+
if (!query && !flags.tag) {
|
|
4106
|
+
process.stderr.write(`${red2("✗")} Please provide a search query.
|
|
4107
|
+
`);
|
|
4108
|
+
process.stderr.write(` Usage: ${bold2("locus skills search <query>")} or ${bold2("locus skills search --tag <tag>")}
|
|
4109
|
+
`);
|
|
4110
|
+
process.exit(1);
|
|
4111
|
+
}
|
|
4112
|
+
let registry;
|
|
4113
|
+
try {
|
|
4114
|
+
registry = await fetchRegistry();
|
|
4115
|
+
} catch (err) {
|
|
4116
|
+
process.stderr.write(`${red2("✗")} Failed to fetch skills registry. Check your internet connection.
|
|
4117
|
+
`);
|
|
4118
|
+
process.stderr.write(` ${dim2(err.message)}
|
|
4119
|
+
`);
|
|
4120
|
+
process.exit(1);
|
|
4121
|
+
}
|
|
4122
|
+
const tagFilter = flags.tag?.toLowerCase();
|
|
4123
|
+
const queryLower = query?.toLowerCase() ?? "";
|
|
4124
|
+
const matches = registry.skills.filter((s) => {
|
|
4125
|
+
if (tagFilter) {
|
|
4126
|
+
return s.tags.some((t) => t.toLowerCase() === tagFilter);
|
|
4127
|
+
}
|
|
4128
|
+
const nameMatch = s.name.toLowerCase().includes(queryLower);
|
|
4129
|
+
const descMatch = s.description.toLowerCase().includes(queryLower);
|
|
4130
|
+
const tagMatch = s.tags.some((t) => t.toLowerCase().includes(queryLower));
|
|
4131
|
+
return nameMatch || descMatch || tagMatch;
|
|
4132
|
+
});
|
|
4133
|
+
if (matches.length === 0) {
|
|
4134
|
+
process.stderr.write(`
|
|
4135
|
+
${yellow2("⚠")} No skills found matching "${bold2(tagFilter || query)}"
|
|
4136
|
+
`);
|
|
4137
|
+
process.stderr.write(` Run ${bold2("locus skills list")} to see all available skills.
|
|
4138
|
+
|
|
4139
|
+
`);
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
4142
|
+
const cwd = process.cwd();
|
|
4143
|
+
const lockFile = readLockFile(cwd);
|
|
4144
|
+
process.stderr.write(`
|
|
4145
|
+
${bold2("Search Results")} for "${cyan2(tagFilter || query)}"
|
|
4146
|
+
|
|
4147
|
+
`);
|
|
4148
|
+
const columns = [
|
|
4149
|
+
{ key: "name", header: "Name", minWidth: 12, maxWidth: 24 },
|
|
4150
|
+
{ key: "description", header: "Description", minWidth: 20, maxWidth: 44 },
|
|
4151
|
+
{
|
|
4152
|
+
key: "tags",
|
|
4153
|
+
header: "Tags",
|
|
4154
|
+
minWidth: 10,
|
|
4155
|
+
maxWidth: 30,
|
|
4156
|
+
format: (val) => dim2(val.join(", "))
|
|
4157
|
+
},
|
|
4158
|
+
{
|
|
4159
|
+
key: "status",
|
|
4160
|
+
header: "Status",
|
|
4161
|
+
minWidth: 8,
|
|
4162
|
+
maxWidth: 12
|
|
4163
|
+
}
|
|
4164
|
+
];
|
|
4165
|
+
const rows = matches.map((s) => ({
|
|
4166
|
+
name: cyan2(s.name),
|
|
4167
|
+
description: s.description,
|
|
4168
|
+
tags: s.tags,
|
|
4169
|
+
status: s.name in lockFile.skills ? green("installed") : dim2("available")
|
|
4170
|
+
}));
|
|
4171
|
+
process.stderr.write(`${renderTable(columns, rows)}
|
|
4172
|
+
|
|
4173
|
+
`);
|
|
4174
|
+
process.stderr.write(` ${dim2(`${matches.length} skill(s) found.`)} Install with: ${bold2("locus skills install <name>")}
|
|
4175
|
+
|
|
4176
|
+
`);
|
|
4177
|
+
}
|
|
3883
4178
|
function printSkillsHelp() {
|
|
3884
4179
|
process.stderr.write(`
|
|
3885
4180
|
${bold2("Usage:")}
|
|
@@ -3888,6 +4183,7 @@ ${bold2("Usage:")}
|
|
|
3888
4183
|
${bold2("Subcommands:")}
|
|
3889
4184
|
${cyan2("list")} List available skills from the registry
|
|
3890
4185
|
${cyan2("list")} ${dim2("--installed")} List locally installed skills
|
|
4186
|
+
${cyan2("search")} ${dim2("<query>")} Search skills by name, description, or tags
|
|
3891
4187
|
${cyan2("install")} ${dim2("<name>")} Install a skill from the registry
|
|
3892
4188
|
${cyan2("remove")} ${dim2("<name>")} Remove an installed skill (alias: ${cyan2("uninstall")})
|
|
3893
4189
|
${cyan2("update")} ${dim2("[name]")} Update installed skill(s) from registry
|
|
@@ -3896,6 +4192,8 @@ ${bold2("Subcommands:")}
|
|
|
3896
4192
|
${bold2("Examples:")}
|
|
3897
4193
|
locus skills list ${dim2("# Browse available skills")}
|
|
3898
4194
|
locus skills list --installed ${dim2("# Show installed skills")}
|
|
4195
|
+
locus skills search "code review" ${dim2("# Search by keyword")}
|
|
4196
|
+
locus skills search --tag testing ${dim2("# Search by tag")}
|
|
3899
4197
|
locus skills install code-review ${dim2("# Install a skill")}
|
|
3900
4198
|
locus skills remove code-review ${dim2("# Remove a skill")}
|
|
3901
4199
|
locus skills update ${dim2("# Update all installed skills")}
|
|
@@ -3940,10 +4238,15 @@ async function skillsCommand(args, flags) {
|
|
|
3940
4238
|
await infoSkill(skillName);
|
|
3941
4239
|
break;
|
|
3942
4240
|
}
|
|
4241
|
+
case "search": {
|
|
4242
|
+
const searchQuery = args.slice(1).join(" ");
|
|
4243
|
+
await searchSkills(searchQuery, flags);
|
|
4244
|
+
break;
|
|
4245
|
+
}
|
|
3943
4246
|
default:
|
|
3944
4247
|
process.stderr.write(`${red2("✗")} Unknown subcommand: ${bold2(subcommand)}
|
|
3945
4248
|
`);
|
|
3946
|
-
process.stderr.write(` Available: ${bold2("list")}, ${bold2("install")}, ${bold2("remove")} (${bold2("uninstall")}), ${bold2("update")}, ${bold2("info")}
|
|
4249
|
+
process.stderr.write(` Available: ${bold2("list")}, ${bold2("search")}, ${bold2("install")}, ${bold2("remove")} (${bold2("uninstall")}), ${bold2("update")}, ${bold2("info")}
|
|
3947
4250
|
`);
|
|
3948
4251
|
process.exit(1);
|
|
3949
4252
|
}
|
|
@@ -4092,16 +4395,16 @@ __export(exports_logs, {
|
|
|
4092
4395
|
logsCommand: () => logsCommand
|
|
4093
4396
|
});
|
|
4094
4397
|
import {
|
|
4095
|
-
existsSync as
|
|
4398
|
+
existsSync as existsSync13,
|
|
4096
4399
|
readdirSync as readdirSync2,
|
|
4097
|
-
readFileSync as
|
|
4400
|
+
readFileSync as readFileSync10,
|
|
4098
4401
|
statSync as statSync2,
|
|
4099
4402
|
unlinkSync as unlinkSync2
|
|
4100
4403
|
} from "node:fs";
|
|
4101
|
-
import { join as
|
|
4404
|
+
import { join as join13 } from "node:path";
|
|
4102
4405
|
async function logsCommand(cwd, options) {
|
|
4103
|
-
const logsDir =
|
|
4104
|
-
if (!
|
|
4406
|
+
const logsDir = join13(cwd, ".locus", "logs");
|
|
4407
|
+
if (!existsSync13(logsDir)) {
|
|
4105
4408
|
process.stderr.write(`${dim2("No logs found.")}
|
|
4106
4409
|
`);
|
|
4107
4410
|
return;
|
|
@@ -4121,7 +4424,7 @@ async function logsCommand(cwd, options) {
|
|
|
4121
4424
|
return viewLog(logFiles[0], options.level, options.lines ?? 50);
|
|
4122
4425
|
}
|
|
4123
4426
|
function viewLog(logFile, levelFilter, maxLines) {
|
|
4124
|
-
const content =
|
|
4427
|
+
const content = readFileSync10(logFile, "utf-8");
|
|
4125
4428
|
const lines = content.trim().split(`
|
|
4126
4429
|
`).filter(Boolean);
|
|
4127
4430
|
process.stderr.write(`
|
|
@@ -4156,9 +4459,9 @@ async function tailLog(logFile, levelFilter) {
|
|
|
4156
4459
|
process.stderr.write(`${bold2("Tailing:")} ${dim2(logFile)} ${dim2("(Ctrl+C to stop)")}
|
|
4157
4460
|
|
|
4158
4461
|
`);
|
|
4159
|
-
let lastSize =
|
|
4160
|
-
if (
|
|
4161
|
-
const content =
|
|
4462
|
+
let lastSize = existsSync13(logFile) ? statSync2(logFile).size : 0;
|
|
4463
|
+
if (existsSync13(logFile)) {
|
|
4464
|
+
const content = readFileSync10(logFile, "utf-8");
|
|
4162
4465
|
const lines = content.trim().split(`
|
|
4163
4466
|
`).filter(Boolean);
|
|
4164
4467
|
const recent = lines.slice(-10);
|
|
@@ -4176,12 +4479,12 @@ async function tailLog(logFile, levelFilter) {
|
|
|
4176
4479
|
}
|
|
4177
4480
|
return new Promise((resolve) => {
|
|
4178
4481
|
const interval = setInterval(() => {
|
|
4179
|
-
if (!
|
|
4482
|
+
if (!existsSync13(logFile))
|
|
4180
4483
|
return;
|
|
4181
4484
|
const currentSize = statSync2(logFile).size;
|
|
4182
4485
|
if (currentSize <= lastSize)
|
|
4183
4486
|
return;
|
|
4184
|
-
const content =
|
|
4487
|
+
const content = readFileSync10(logFile, "utf-8");
|
|
4185
4488
|
const allLines = content.trim().split(`
|
|
4186
4489
|
`).filter(Boolean);
|
|
4187
4490
|
const oldContent = content.slice(0, lastSize);
|
|
@@ -4236,7 +4539,7 @@ function cleanLogs(logsDir) {
|
|
|
4236
4539
|
`);
|
|
4237
4540
|
}
|
|
4238
4541
|
function getLogFiles(logsDir) {
|
|
4239
|
-
return readdirSync2(logsDir).filter((f) => f.startsWith("locus-") && f.endsWith(".log")).map((f) =>
|
|
4542
|
+
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
4543
|
}
|
|
4241
4544
|
function formatEntry(entry) {
|
|
4242
4545
|
const time = dim2(new Date(entry.ts).toLocaleTimeString());
|
|
@@ -4546,9 +4849,9 @@ var init_stream_renderer = __esm(() => {
|
|
|
4546
4849
|
|
|
4547
4850
|
// src/repl/clipboard.ts
|
|
4548
4851
|
import { execSync as execSync6 } from "node:child_process";
|
|
4549
|
-
import { existsSync as
|
|
4852
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync9 } from "node:fs";
|
|
4550
4853
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
4551
|
-
import { join as
|
|
4854
|
+
import { join as join14 } from "node:path";
|
|
4552
4855
|
function readClipboardImage() {
|
|
4553
4856
|
if (process.platform === "darwin") {
|
|
4554
4857
|
return readMacOSClipboardImage();
|
|
@@ -4559,14 +4862,14 @@ function readClipboardImage() {
|
|
|
4559
4862
|
return null;
|
|
4560
4863
|
}
|
|
4561
4864
|
function ensureStableDir() {
|
|
4562
|
-
if (!
|
|
4865
|
+
if (!existsSync14(STABLE_DIR)) {
|
|
4563
4866
|
mkdirSync9(STABLE_DIR, { recursive: true });
|
|
4564
4867
|
}
|
|
4565
4868
|
}
|
|
4566
4869
|
function readMacOSClipboardImage() {
|
|
4567
4870
|
try {
|
|
4568
4871
|
ensureStableDir();
|
|
4569
|
-
const destPath =
|
|
4872
|
+
const destPath = join14(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
4570
4873
|
const script = [
|
|
4571
4874
|
`set destPath to POSIX file "${destPath}"`,
|
|
4572
4875
|
"try",
|
|
@@ -4590,7 +4893,7 @@ function readMacOSClipboardImage() {
|
|
|
4590
4893
|
timeout: 5000,
|
|
4591
4894
|
stdio: ["pipe", "pipe", "pipe"]
|
|
4592
4895
|
}).trim();
|
|
4593
|
-
if (result === "ok" &&
|
|
4896
|
+
if (result === "ok" && existsSync14(destPath)) {
|
|
4594
4897
|
return destPath;
|
|
4595
4898
|
}
|
|
4596
4899
|
} catch {}
|
|
@@ -4603,9 +4906,9 @@ function readLinuxClipboardImage() {
|
|
|
4603
4906
|
return null;
|
|
4604
4907
|
}
|
|
4605
4908
|
ensureStableDir();
|
|
4606
|
-
const destPath =
|
|
4909
|
+
const destPath = join14(STABLE_DIR, `clipboard-${Date.now()}.png`);
|
|
4607
4910
|
execSync6(`xclip -selection clipboard -t image/png -o > "${destPath}" 2>/dev/null`, { timeout: 5000 });
|
|
4608
|
-
if (
|
|
4911
|
+
if (existsSync14(destPath)) {
|
|
4609
4912
|
return destPath;
|
|
4610
4913
|
}
|
|
4611
4914
|
} catch {}
|
|
@@ -4613,13 +4916,13 @@ function readLinuxClipboardImage() {
|
|
|
4613
4916
|
}
|
|
4614
4917
|
var STABLE_DIR;
|
|
4615
4918
|
var init_clipboard = __esm(() => {
|
|
4616
|
-
STABLE_DIR =
|
|
4919
|
+
STABLE_DIR = join14(tmpdir2(), "locus-images");
|
|
4617
4920
|
});
|
|
4618
4921
|
|
|
4619
4922
|
// src/repl/image-detect.ts
|
|
4620
|
-
import { copyFileSync, existsSync as
|
|
4923
|
+
import { copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "node:fs";
|
|
4621
4924
|
import { homedir as homedir3, tmpdir as tmpdir3 } from "node:os";
|
|
4622
|
-
import { basename, extname, join as
|
|
4925
|
+
import { basename, extname, join as join15, resolve } from "node:path";
|
|
4623
4926
|
function detectImages(input) {
|
|
4624
4927
|
const detected = [];
|
|
4625
4928
|
const byResolved = new Map;
|
|
@@ -4713,15 +5016,15 @@ function collectReferencedAttachments(input, attachments) {
|
|
|
4713
5016
|
return dedupeByResolvedPath(selected);
|
|
4714
5017
|
}
|
|
4715
5018
|
function relocateImages(images, projectRoot) {
|
|
4716
|
-
const targetDir =
|
|
5019
|
+
const targetDir = join15(projectRoot, ".locus", "tmp", "images");
|
|
4717
5020
|
for (const img of images) {
|
|
4718
5021
|
if (!img.exists)
|
|
4719
5022
|
continue;
|
|
4720
5023
|
try {
|
|
4721
|
-
if (!
|
|
5024
|
+
if (!existsSync15(targetDir)) {
|
|
4722
5025
|
mkdirSync10(targetDir, { recursive: true });
|
|
4723
5026
|
}
|
|
4724
|
-
const dest =
|
|
5027
|
+
const dest = join15(targetDir, basename(img.stablePath));
|
|
4725
5028
|
copyFileSync(img.stablePath, dest);
|
|
4726
5029
|
img.stablePath = dest;
|
|
4727
5030
|
} catch {}
|
|
@@ -4733,7 +5036,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
4733
5036
|
return;
|
|
4734
5037
|
let resolved = stripQuotes(rawPath).replace(/\\ /g, " ");
|
|
4735
5038
|
if (resolved.startsWith("~/")) {
|
|
4736
|
-
resolved =
|
|
5039
|
+
resolved = join15(homedir3(), resolved.slice(2));
|
|
4737
5040
|
}
|
|
4738
5041
|
resolved = resolve(resolved);
|
|
4739
5042
|
const existing = byResolved.get(resolved);
|
|
@@ -4746,7 +5049,7 @@ function addIfImage(rawPath, rawMatch, detected, byResolved) {
|
|
|
4746
5049
|
]);
|
|
4747
5050
|
return;
|
|
4748
5051
|
}
|
|
4749
|
-
const exists =
|
|
5052
|
+
const exists = existsSync15(resolved);
|
|
4750
5053
|
let stablePath = resolved;
|
|
4751
5054
|
if (exists) {
|
|
4752
5055
|
stablePath = copyToStable(resolved);
|
|
@@ -4800,10 +5103,10 @@ function dedupeByResolvedPath(images) {
|
|
|
4800
5103
|
}
|
|
4801
5104
|
function copyToStable(sourcePath) {
|
|
4802
5105
|
try {
|
|
4803
|
-
if (!
|
|
5106
|
+
if (!existsSync15(STABLE_DIR2)) {
|
|
4804
5107
|
mkdirSync10(STABLE_DIR2, { recursive: true });
|
|
4805
5108
|
}
|
|
4806
|
-
const dest =
|
|
5109
|
+
const dest = join15(STABLE_DIR2, `${Date.now()}-${basename(sourcePath)}`);
|
|
4807
5110
|
copyFileSync(sourcePath, dest);
|
|
4808
5111
|
return dest;
|
|
4809
5112
|
} catch {
|
|
@@ -4823,7 +5126,7 @@ var init_image_detect = __esm(() => {
|
|
|
4823
5126
|
".tif",
|
|
4824
5127
|
".tiff"
|
|
4825
5128
|
]);
|
|
4826
|
-
STABLE_DIR2 =
|
|
5129
|
+
STABLE_DIR2 = join15(tmpdir3(), "locus-images");
|
|
4827
5130
|
PLACEHOLDER_ID_PATTERN = /\(locus:\/\/screenshot-(\d+)\)/g;
|
|
4828
5131
|
});
|
|
4829
5132
|
|
|
@@ -5832,21 +6135,21 @@ var init_claude = __esm(() => {
|
|
|
5832
6135
|
import { exec } from "node:child_process";
|
|
5833
6136
|
import {
|
|
5834
6137
|
cpSync,
|
|
5835
|
-
existsSync as
|
|
6138
|
+
existsSync as existsSync16,
|
|
5836
6139
|
mkdirSync as mkdirSync11,
|
|
5837
6140
|
mkdtempSync,
|
|
5838
6141
|
readdirSync as readdirSync3,
|
|
5839
|
-
readFileSync as
|
|
6142
|
+
readFileSync as readFileSync11,
|
|
5840
6143
|
rmSync as rmSync2,
|
|
5841
6144
|
statSync as statSync3
|
|
5842
6145
|
} from "node:fs";
|
|
5843
6146
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
5844
|
-
import { dirname as dirname3, join as
|
|
6147
|
+
import { dirname as dirname3, join as join16, relative } from "node:path";
|
|
5845
6148
|
import { promisify } from "node:util";
|
|
5846
6149
|
function parseIgnoreFile(filePath) {
|
|
5847
|
-
if (!
|
|
6150
|
+
if (!existsSync16(filePath))
|
|
5848
6151
|
return [];
|
|
5849
|
-
const content =
|
|
6152
|
+
const content = readFileSync11(filePath, "utf-8");
|
|
5850
6153
|
const rules = [];
|
|
5851
6154
|
for (const rawLine of content.split(`
|
|
5852
6155
|
`)) {
|
|
@@ -5914,14 +6217,14 @@ function findIgnoredPaths(projectRoot, rules) {
|
|
|
5914
6217
|
for (const name of entries) {
|
|
5915
6218
|
if (SKIP_DIRS.has(name))
|
|
5916
6219
|
continue;
|
|
5917
|
-
const fullPath =
|
|
5918
|
-
let
|
|
6220
|
+
const fullPath = join16(dir, name);
|
|
6221
|
+
let stat2 = null;
|
|
5919
6222
|
try {
|
|
5920
|
-
|
|
6223
|
+
stat2 = statSync3(fullPath);
|
|
5921
6224
|
} catch {
|
|
5922
6225
|
continue;
|
|
5923
6226
|
}
|
|
5924
|
-
const isDir =
|
|
6227
|
+
const isDir = stat2.isDirectory();
|
|
5925
6228
|
let matched = false;
|
|
5926
6229
|
for (const m of positiveMatchers) {
|
|
5927
6230
|
if (m.isDirectory && !isDir)
|
|
@@ -5948,7 +6251,7 @@ function findIgnoredPaths(projectRoot, rules) {
|
|
|
5948
6251
|
}
|
|
5949
6252
|
function backupIgnoredFiles(projectRoot) {
|
|
5950
6253
|
const log = getLogger();
|
|
5951
|
-
const ignorePath =
|
|
6254
|
+
const ignorePath = join16(projectRoot, ".sandboxignore");
|
|
5952
6255
|
const rules = parseIgnoreFile(ignorePath);
|
|
5953
6256
|
if (rules.length === 0)
|
|
5954
6257
|
return NOOP_BACKUP;
|
|
@@ -5957,7 +6260,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5957
6260
|
return NOOP_BACKUP;
|
|
5958
6261
|
let backupDir;
|
|
5959
6262
|
try {
|
|
5960
|
-
backupDir = mkdtempSync(
|
|
6263
|
+
backupDir = mkdtempSync(join16(tmpdir4(), "locus-sandbox-backup-"));
|
|
5961
6264
|
} catch (err) {
|
|
5962
6265
|
log.debug("Failed to create sandbox backup dir", {
|
|
5963
6266
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -5967,7 +6270,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
5967
6270
|
const backed = [];
|
|
5968
6271
|
for (const src of paths) {
|
|
5969
6272
|
const rel = relative(projectRoot, src);
|
|
5970
|
-
const dest =
|
|
6273
|
+
const dest = join16(backupDir, rel);
|
|
5971
6274
|
try {
|
|
5972
6275
|
mkdirSync11(dirname3(dest), { recursive: true });
|
|
5973
6276
|
cpSync(src, dest, { recursive: true, preserveTimestamps: true });
|
|
@@ -6009,7 +6312,7 @@ function backupIgnoredFiles(projectRoot) {
|
|
|
6009
6312
|
}
|
|
6010
6313
|
async function enforceSandboxIgnore(sandboxName, projectRoot, containerWorkdir) {
|
|
6011
6314
|
const log = getLogger();
|
|
6012
|
-
const ignorePath =
|
|
6315
|
+
const ignorePath = join16(projectRoot, ".sandboxignore");
|
|
6013
6316
|
const rules = parseIgnoreFile(ignorePath);
|
|
6014
6317
|
if (rules.length === 0)
|
|
6015
6318
|
return;
|
|
@@ -8173,14 +8476,113 @@ var init_sprint = __esm(() => {
|
|
|
8173
8476
|
init_terminal();
|
|
8174
8477
|
});
|
|
8175
8478
|
|
|
8479
|
+
// src/skills/matcher.ts
|
|
8480
|
+
function matchRelevantSkills(issue, skills) {
|
|
8481
|
+
if (skills.length <= MAX_SKILLS)
|
|
8482
|
+
return skills;
|
|
8483
|
+
const titleTokens = tokenize(issue.title);
|
|
8484
|
+
const bodyTokens = tokenize(issue.body).slice(0, 200);
|
|
8485
|
+
const labelTokens = issue.labels.flatMap((l) => tokenize(l));
|
|
8486
|
+
const scored = skills.map((skill) => {
|
|
8487
|
+
let score = 0;
|
|
8488
|
+
const tagSet = new Set(skill.tags.map((t) => t.toLowerCase()));
|
|
8489
|
+
const descTokens = new Set(tokenize(skill.description));
|
|
8490
|
+
for (const lt of labelTokens) {
|
|
8491
|
+
if (tagSet.has(lt))
|
|
8492
|
+
score += 5;
|
|
8493
|
+
}
|
|
8494
|
+
for (const tw of titleTokens) {
|
|
8495
|
+
if (tagSet.has(tw))
|
|
8496
|
+
score += 3;
|
|
8497
|
+
}
|
|
8498
|
+
for (const bw of bodyTokens) {
|
|
8499
|
+
if (tagSet.has(bw))
|
|
8500
|
+
score += 2;
|
|
8501
|
+
}
|
|
8502
|
+
for (const tw of titleTokens) {
|
|
8503
|
+
if (descTokens.has(tw))
|
|
8504
|
+
score += 1;
|
|
8505
|
+
}
|
|
8506
|
+
for (const bw of bodyTokens) {
|
|
8507
|
+
if (descTokens.has(bw))
|
|
8508
|
+
score += 0.5;
|
|
8509
|
+
}
|
|
8510
|
+
return { skill, score };
|
|
8511
|
+
});
|
|
8512
|
+
scored.sort((a, b) => b.score - a.score);
|
|
8513
|
+
const relevant = scored.filter((s) => s.score >= RELEVANCE_THRESHOLD);
|
|
8514
|
+
if (relevant.length > 0) {
|
|
8515
|
+
return relevant.slice(0, MAX_SKILLS).map((s) => s.skill);
|
|
8516
|
+
}
|
|
8517
|
+
return scored.slice(0, FALLBACK_COUNT).map((s) => s.skill);
|
|
8518
|
+
}
|
|
8519
|
+
function tokenize(text) {
|
|
8520
|
+
if (!text)
|
|
8521
|
+
return [];
|
|
8522
|
+
return text.toLowerCase().replace(/[^a-z0-9\-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
|
|
8523
|
+
}
|
|
8524
|
+
var RELEVANCE_THRESHOLD = 2, MAX_SKILLS = 5, FALLBACK_COUNT = 3, STOP_WORDS;
|
|
8525
|
+
var init_matcher = __esm(() => {
|
|
8526
|
+
STOP_WORDS = new Set([
|
|
8527
|
+
"a",
|
|
8528
|
+
"an",
|
|
8529
|
+
"the",
|
|
8530
|
+
"is",
|
|
8531
|
+
"it",
|
|
8532
|
+
"to",
|
|
8533
|
+
"in",
|
|
8534
|
+
"on",
|
|
8535
|
+
"of",
|
|
8536
|
+
"for",
|
|
8537
|
+
"and",
|
|
8538
|
+
"or",
|
|
8539
|
+
"not",
|
|
8540
|
+
"with",
|
|
8541
|
+
"this",
|
|
8542
|
+
"that",
|
|
8543
|
+
"from",
|
|
8544
|
+
"by",
|
|
8545
|
+
"as",
|
|
8546
|
+
"at",
|
|
8547
|
+
"be",
|
|
8548
|
+
"we",
|
|
8549
|
+
"i",
|
|
8550
|
+
"you",
|
|
8551
|
+
"my",
|
|
8552
|
+
"our",
|
|
8553
|
+
"do",
|
|
8554
|
+
"if",
|
|
8555
|
+
"no",
|
|
8556
|
+
"so",
|
|
8557
|
+
"up",
|
|
8558
|
+
"can",
|
|
8559
|
+
"all",
|
|
8560
|
+
"but",
|
|
8561
|
+
"has",
|
|
8562
|
+
"had",
|
|
8563
|
+
"have",
|
|
8564
|
+
"will",
|
|
8565
|
+
"should",
|
|
8566
|
+
"would",
|
|
8567
|
+
"could",
|
|
8568
|
+
"need",
|
|
8569
|
+
"want",
|
|
8570
|
+
"also",
|
|
8571
|
+
"new",
|
|
8572
|
+
"use",
|
|
8573
|
+
"add",
|
|
8574
|
+
"make"
|
|
8575
|
+
]);
|
|
8576
|
+
});
|
|
8577
|
+
|
|
8176
8578
|
// src/core/prompt-builder.ts
|
|
8177
8579
|
import { execSync as execSync9 } from "node:child_process";
|
|
8178
|
-
import { existsSync as
|
|
8179
|
-
import { join as
|
|
8580
|
+
import { existsSync as existsSync17, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "node:fs";
|
|
8581
|
+
import { join as join17 } from "node:path";
|
|
8180
8582
|
function buildExecutionPrompt(ctx) {
|
|
8181
8583
|
const sections = [];
|
|
8182
8584
|
sections.push(buildSystemContext(ctx.projectRoot));
|
|
8183
|
-
const skills = buildSkillsContext(ctx.projectRoot);
|
|
8585
|
+
const skills = buildSkillsContext(ctx.projectRoot, ctx.issue);
|
|
8184
8586
|
if (skills)
|
|
8185
8587
|
sections.push(skills);
|
|
8186
8588
|
sections.push(buildTaskContext(ctx.issue, ctx.issueComments));
|
|
@@ -8198,7 +8600,7 @@ function buildExecutionPrompt(ctx) {
|
|
|
8198
8600
|
function buildFeedbackPrompt(ctx) {
|
|
8199
8601
|
const sections = [];
|
|
8200
8602
|
sections.push(buildSystemContext(ctx.projectRoot));
|
|
8201
|
-
const skills = buildSkillsContext(ctx.projectRoot);
|
|
8603
|
+
const skills = buildSkillsContext(ctx.projectRoot, ctx.issue);
|
|
8202
8604
|
if (skills)
|
|
8203
8605
|
sections.push(skills);
|
|
8204
8606
|
sections.push(buildTaskContext(ctx.issue));
|
|
@@ -8212,15 +8614,22 @@ function buildFeedbackPrompt(ctx) {
|
|
|
8212
8614
|
}
|
|
8213
8615
|
function buildReplPrompt(userMessage, projectRoot, _config, previousMessages) {
|
|
8214
8616
|
const sections = [];
|
|
8215
|
-
const locusmd = readFileSafe(
|
|
8617
|
+
const locusmd = readFileSafe(join17(projectRoot, ".locus", "LOCUS.md"));
|
|
8216
8618
|
if (locusmd) {
|
|
8217
8619
|
sections.push(`<project-instructions>
|
|
8218
8620
|
${locusmd}
|
|
8219
8621
|
</project-instructions>`);
|
|
8220
8622
|
}
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8623
|
+
const memory = loadMemoryContent(projectRoot);
|
|
8624
|
+
if (memory) {
|
|
8625
|
+
sections.push(`<past-learnings>
|
|
8626
|
+
${memory}
|
|
8627
|
+
</past-learnings>`);
|
|
8628
|
+
} else {
|
|
8629
|
+
sections.push(`<past-learnings>
|
|
8630
|
+
No past learnings recorded yet.</past-learnings>`);
|
|
8631
|
+
}
|
|
8632
|
+
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
8633
|
if (previousMessages && previousMessages.length > 0) {
|
|
8225
8634
|
const recent = previousMessages.slice(-10);
|
|
8226
8635
|
const historyLines = recent.map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`);
|
|
@@ -8239,22 +8648,41 @@ ${userMessage}
|
|
|
8239
8648
|
|
|
8240
8649
|
`);
|
|
8241
8650
|
}
|
|
8651
|
+
function loadMemoryContent(projectRoot) {
|
|
8652
|
+
const memoryDir = getMemoryDir(projectRoot);
|
|
8653
|
+
if (existsSync17(memoryDir)) {
|
|
8654
|
+
const content = readAllMemorySync(projectRoot);
|
|
8655
|
+
if (content.trim()) {
|
|
8656
|
+
return content.length > MEMORY_MAX_CHARS ? `${content.slice(0, MEMORY_MAX_CHARS)}
|
|
8657
|
+
|
|
8658
|
+
...(truncated)` : content;
|
|
8659
|
+
}
|
|
8660
|
+
}
|
|
8661
|
+
return "";
|
|
8662
|
+
}
|
|
8242
8663
|
function buildSystemContext(projectRoot) {
|
|
8243
8664
|
const parts = [];
|
|
8244
|
-
const locusmd = readFileSafe(
|
|
8665
|
+
const locusmd = readFileSafe(join17(projectRoot, ".locus", "LOCUS.md"));
|
|
8245
8666
|
if (locusmd) {
|
|
8246
8667
|
parts.push(`<project-instructions>
|
|
8247
8668
|
${locusmd}
|
|
8248
8669
|
</project-instructions>`);
|
|
8249
8670
|
}
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8671
|
+
const memory = loadMemoryContent(projectRoot);
|
|
8672
|
+
if (memory) {
|
|
8673
|
+
parts.push(`<past-learnings>
|
|
8674
|
+
${memory}
|
|
8675
|
+
</past-learnings>`);
|
|
8676
|
+
} else {
|
|
8677
|
+
parts.push(`<past-learnings>
|
|
8678
|
+
No past learnings recorded yet.</past-learnings>`);
|
|
8679
|
+
}
|
|
8680
|
+
const discussionsDir = join17(projectRoot, ".locus", "discussions");
|
|
8681
|
+
if (existsSync17(discussionsDir)) {
|
|
8254
8682
|
try {
|
|
8255
8683
|
const files = readdirSync4(discussionsDir).filter((f) => f.endsWith(".md")).slice(0, 3);
|
|
8256
8684
|
for (const file of files) {
|
|
8257
|
-
const content = readFileSafe(
|
|
8685
|
+
const content = readFileSafe(join17(discussionsDir, file));
|
|
8258
8686
|
if (content) {
|
|
8259
8687
|
const name = file.replace(".md", "");
|
|
8260
8688
|
parts.push(`<discussion name="${name}">
|
|
@@ -8363,7 +8791,7 @@ function buildExecutionRules(config) {
|
|
|
8363
8791
|
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
8792
|
2. **Code quality:** Follow existing code style. Run linters/formatters if available.
|
|
8365
8793
|
3. **Testing:** If test files exist for modified code, update them accordingly.
|
|
8366
|
-
4. **Update
|
|
8794
|
+
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
8795
|
5. **Do NOT:**
|
|
8368
8796
|
- Run \`git push\` (the orchestrator handles pushing)
|
|
8369
8797
|
- Modify files outside the scope of this issue
|
|
@@ -8401,13 +8829,13 @@ function buildFeedbackInstructions() {
|
|
|
8401
8829
|
2. Make targeted changes — do NOT rewrite code from scratch.
|
|
8402
8830
|
3. If a reviewer comment is unclear, make your best judgment and note your interpretation.
|
|
8403
8831
|
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),
|
|
8832
|
+
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
8833
|
6. When done, summarize what you changed in response to each comment.
|
|
8406
8834
|
</instructions>`;
|
|
8407
8835
|
}
|
|
8408
|
-
function buildSkillsContext(projectRoot) {
|
|
8409
|
-
const skillsDir =
|
|
8410
|
-
if (!
|
|
8836
|
+
function buildSkillsContext(projectRoot, issue) {
|
|
8837
|
+
const skillsDir = join17(projectRoot, CLAUDE_SKILLS_DIR);
|
|
8838
|
+
if (!existsSync17(skillsDir))
|
|
8411
8839
|
return null;
|
|
8412
8840
|
let dirs;
|
|
8413
8841
|
try {
|
|
@@ -8417,21 +8845,35 @@ function buildSkillsContext(projectRoot) {
|
|
|
8417
8845
|
}
|
|
8418
8846
|
if (dirs.length === 0)
|
|
8419
8847
|
return null;
|
|
8420
|
-
|
|
8848
|
+
let allSkills = [];
|
|
8421
8849
|
for (const dir of dirs) {
|
|
8422
|
-
const skillPath =
|
|
8850
|
+
const skillPath = join17(skillsDir, dir, "SKILL.md");
|
|
8423
8851
|
const content = readFileSafe(skillPath);
|
|
8424
8852
|
if (!content)
|
|
8425
8853
|
continue;
|
|
8426
8854
|
const fm = parseFrontmatter(content);
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8855
|
+
allSkills.push({
|
|
8856
|
+
dir,
|
|
8857
|
+
name: fm.name || dir,
|
|
8858
|
+
description: fm.description || "",
|
|
8859
|
+
tags: fm.tags ? fm.tags.split(",").map((t) => t.trim()) : []
|
|
8860
|
+
});
|
|
8430
8861
|
}
|
|
8431
|
-
if (
|
|
8862
|
+
if (allSkills.length === 0)
|
|
8432
8863
|
return null;
|
|
8864
|
+
let selectedSkills = allSkills;
|
|
8865
|
+
if (issue) {
|
|
8866
|
+
const issueCtx = {
|
|
8867
|
+
title: issue.title,
|
|
8868
|
+
body: issue.body || "",
|
|
8869
|
+
labels: issue.labels
|
|
8870
|
+
};
|
|
8871
|
+
const relevant = matchRelevantSkills(issueCtx, allSkills);
|
|
8872
|
+
selectedSkills = allSkills.filter((s) => relevant.some((r) => r.name === s.name));
|
|
8873
|
+
}
|
|
8874
|
+
const entries = selectedSkills.map((s) => `- **${s.name}**: ${s.description}`);
|
|
8433
8875
|
return `<installed-skills>
|
|
8434
|
-
The following skills are installed
|
|
8876
|
+
The following skills are installed and relevant to this task. Read the full instructions from \`.claude/skills/<name>/SKILL.md\` before using a skill.
|
|
8435
8877
|
|
|
8436
8878
|
${entries.join(`
|
|
8437
8879
|
`)}
|
|
@@ -8448,22 +8890,30 @@ function parseFrontmatter(content) {
|
|
|
8448
8890
|
if (idx === -1)
|
|
8449
8891
|
continue;
|
|
8450
8892
|
const key = line.slice(0, idx).trim();
|
|
8451
|
-
|
|
8452
|
-
if (key
|
|
8453
|
-
|
|
8893
|
+
let val = line.slice(idx + 1).trim();
|
|
8894
|
+
if (!key || !val)
|
|
8895
|
+
continue;
|
|
8896
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
8897
|
+
val = val.slice(1, -1);
|
|
8898
|
+
}
|
|
8899
|
+
result[key] = val;
|
|
8454
8900
|
}
|
|
8455
8901
|
return result;
|
|
8456
8902
|
}
|
|
8457
8903
|
function readFileSafe(path) {
|
|
8458
8904
|
try {
|
|
8459
|
-
if (!
|
|
8905
|
+
if (!existsSync17(path))
|
|
8460
8906
|
return null;
|
|
8461
|
-
return
|
|
8907
|
+
return readFileSync12(path, "utf-8");
|
|
8462
8908
|
} catch {
|
|
8463
8909
|
return null;
|
|
8464
8910
|
}
|
|
8465
8911
|
}
|
|
8466
|
-
var
|
|
8912
|
+
var MEMORY_MAX_CHARS = 4000;
|
|
8913
|
+
var init_prompt_builder = __esm(() => {
|
|
8914
|
+
init_matcher();
|
|
8915
|
+
init_memory();
|
|
8916
|
+
});
|
|
8467
8917
|
|
|
8468
8918
|
// src/display/json-stream.ts
|
|
8469
8919
|
class JsonStream {
|
|
@@ -8555,6 +9005,180 @@ class JsonStream {
|
|
|
8555
9005
|
}
|
|
8556
9006
|
}
|
|
8557
9007
|
|
|
9008
|
+
// src/core/memory-capture.ts
|
|
9009
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
9010
|
+
function prepareTranscript(messages) {
|
|
9011
|
+
if (!messages || messages.length === 0)
|
|
9012
|
+
return "";
|
|
9013
|
+
const lines = [];
|
|
9014
|
+
for (const msg of messages) {
|
|
9015
|
+
const label = msg.role === "user" ? "User" : "Assistant";
|
|
9016
|
+
lines.push(`### ${label}
|
|
9017
|
+
${msg.content}`);
|
|
9018
|
+
}
|
|
9019
|
+
let transcript = lines.join(`
|
|
9020
|
+
|
|
9021
|
+
`);
|
|
9022
|
+
if (transcript.length > TRANSCRIPT_MAX_CHARS) {
|
|
9023
|
+
transcript = `${transcript.slice(0, TRANSCRIPT_MAX_CHARS)}
|
|
9024
|
+
|
|
9025
|
+
...(truncated)`;
|
|
9026
|
+
}
|
|
9027
|
+
return transcript;
|
|
9028
|
+
}
|
|
9029
|
+
function buildExtractionPrompt(transcript, existingMemory) {
|
|
9030
|
+
const categoryList = Object.entries(MEMORY_CATEGORIES).map(([key, meta]) => `- "${key}": ${meta.title} — ${meta.description}`).join(`
|
|
9031
|
+
`);
|
|
9032
|
+
return `You are a memory extraction assistant. Extract project-level reusable lessons from the following session transcript.
|
|
9033
|
+
|
|
9034
|
+
## Valid Categories
|
|
9035
|
+
|
|
9036
|
+
${categoryList}
|
|
9037
|
+
|
|
9038
|
+
## Existing Memory (for deduplication)
|
|
9039
|
+
|
|
9040
|
+
Do NOT extract entries that duplicate or closely overlap with these existing entries:
|
|
9041
|
+
|
|
9042
|
+
<existing-memory>
|
|
9043
|
+
${existingMemory || "(none)"}
|
|
9044
|
+
</existing-memory>
|
|
9045
|
+
|
|
9046
|
+
## Quality Bar
|
|
9047
|
+
|
|
9048
|
+
Only extract entries that would help a new agent on a future task. Skip:
|
|
9049
|
+
- Session-specific details or in-progress work
|
|
9050
|
+
- One-time fixes or trivial observations
|
|
9051
|
+
- Speculative or unverified conclusions
|
|
9052
|
+
- Anything that duplicates existing memory entries above
|
|
9053
|
+
|
|
9054
|
+
## Output Format
|
|
9055
|
+
|
|
9056
|
+
Respond with ONLY a JSON array. No markdown fencing, no explanation. Example:
|
|
9057
|
+
[{"category": "architecture", "text": "SDK types are shared via @locusai/shared package"}]
|
|
9058
|
+
|
|
9059
|
+
If there are no extractable lessons, respond with: []
|
|
9060
|
+
|
|
9061
|
+
## Session Transcript
|
|
9062
|
+
|
|
9063
|
+
${transcript}`;
|
|
9064
|
+
}
|
|
9065
|
+
function callExtractionAI(prompt, model) {
|
|
9066
|
+
return new Promise((resolve2) => {
|
|
9067
|
+
const args = [
|
|
9068
|
+
"--print",
|
|
9069
|
+
"--dangerously-skip-permissions",
|
|
9070
|
+
"--no-session-persistence"
|
|
9071
|
+
];
|
|
9072
|
+
if (model) {
|
|
9073
|
+
args.push("--model", model);
|
|
9074
|
+
}
|
|
9075
|
+
const env = { ...process.env };
|
|
9076
|
+
delete env.CLAUDECODE;
|
|
9077
|
+
delete env.CLAUDE_CODE;
|
|
9078
|
+
const proc = spawn6("claude", args, {
|
|
9079
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
9080
|
+
env
|
|
9081
|
+
});
|
|
9082
|
+
let output = "";
|
|
9083
|
+
let errorOutput = "";
|
|
9084
|
+
proc.stdout?.on("data", (chunk) => {
|
|
9085
|
+
output += chunk.toString();
|
|
9086
|
+
});
|
|
9087
|
+
proc.stderr?.on("data", (chunk) => {
|
|
9088
|
+
errorOutput += chunk.toString();
|
|
9089
|
+
});
|
|
9090
|
+
proc.on("close", (code) => {
|
|
9091
|
+
if (code === 0) {
|
|
9092
|
+
resolve2({ success: true, output });
|
|
9093
|
+
} else {
|
|
9094
|
+
resolve2({
|
|
9095
|
+
success: false,
|
|
9096
|
+
output,
|
|
9097
|
+
error: errorOutput || `claude exited with code ${code}`
|
|
9098
|
+
});
|
|
9099
|
+
}
|
|
9100
|
+
});
|
|
9101
|
+
proc.on("error", (err) => {
|
|
9102
|
+
resolve2({
|
|
9103
|
+
success: false,
|
|
9104
|
+
output: "",
|
|
9105
|
+
error: `Failed to spawn claude: ${err.message}`
|
|
9106
|
+
});
|
|
9107
|
+
});
|
|
9108
|
+
proc.stdin?.write(prompt);
|
|
9109
|
+
proc.stdin?.end();
|
|
9110
|
+
});
|
|
9111
|
+
}
|
|
9112
|
+
function parseExtractionResponse(raw) {
|
|
9113
|
+
let text = raw.trim();
|
|
9114
|
+
const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
9115
|
+
if (fenceMatch) {
|
|
9116
|
+
text = fenceMatch[1].trim();
|
|
9117
|
+
}
|
|
9118
|
+
const arrayMatch = text.match(/\[[\s\S]*\]/);
|
|
9119
|
+
if (!arrayMatch)
|
|
9120
|
+
return [];
|
|
9121
|
+
const parsed = JSON.parse(arrayMatch[0]);
|
|
9122
|
+
if (!Array.isArray(parsed))
|
|
9123
|
+
return [];
|
|
9124
|
+
return parsed.filter((item) => typeof item === "object" && item !== null && typeof item.category === "string" && typeof item.text === "string");
|
|
9125
|
+
}
|
|
9126
|
+
async function captureMemoryFromSession(projectRoot, transcript, config) {
|
|
9127
|
+
const log = getLogger();
|
|
9128
|
+
try {
|
|
9129
|
+
if (!transcript.trim()) {
|
|
9130
|
+
return { captured: 0 };
|
|
9131
|
+
}
|
|
9132
|
+
const existingMemory = await readAllMemory(projectRoot);
|
|
9133
|
+
const prompt = buildExtractionPrompt(transcript, existingMemory);
|
|
9134
|
+
const result = await callExtractionAI(prompt, config.model);
|
|
9135
|
+
if (!result.success) {
|
|
9136
|
+
log.warn("Memory capture: AI call failed", { error: result.error });
|
|
9137
|
+
return { captured: 0 };
|
|
9138
|
+
}
|
|
9139
|
+
let entries;
|
|
9140
|
+
try {
|
|
9141
|
+
entries = parseExtractionResponse(result.output);
|
|
9142
|
+
} catch (e) {
|
|
9143
|
+
log.warn("Memory capture: failed to parse AI response", {
|
|
9144
|
+
error: e instanceof Error ? e.message : String(e)
|
|
9145
|
+
});
|
|
9146
|
+
return { captured: 0 };
|
|
9147
|
+
}
|
|
9148
|
+
if (entries.length === 0) {
|
|
9149
|
+
return { captured: 0 };
|
|
9150
|
+
}
|
|
9151
|
+
const validEntries = entries.filter((entry) => {
|
|
9152
|
+
if (!VALID_CATEGORIES.has(entry.category)) {
|
|
9153
|
+
log.debug("Memory capture: skipping invalid category", {
|
|
9154
|
+
category: entry.category
|
|
9155
|
+
});
|
|
9156
|
+
return false;
|
|
9157
|
+
}
|
|
9158
|
+
return true;
|
|
9159
|
+
});
|
|
9160
|
+
if (validEntries.length === 0) {
|
|
9161
|
+
return { captured: 0 };
|
|
9162
|
+
}
|
|
9163
|
+
await appendMemoryEntries(projectRoot, validEntries);
|
|
9164
|
+
log.debug("Memory capture: extracted entries", {
|
|
9165
|
+
count: validEntries.length
|
|
9166
|
+
});
|
|
9167
|
+
return { captured: validEntries.length };
|
|
9168
|
+
} catch (e) {
|
|
9169
|
+
log.warn("Memory capture: unexpected error", {
|
|
9170
|
+
error: e instanceof Error ? e.message : String(e)
|
|
9171
|
+
});
|
|
9172
|
+
return { captured: 0 };
|
|
9173
|
+
}
|
|
9174
|
+
}
|
|
9175
|
+
var TRANSCRIPT_MAX_CHARS = 32000, VALID_CATEGORIES;
|
|
9176
|
+
var init_memory_capture = __esm(() => {
|
|
9177
|
+
init_logger();
|
|
9178
|
+
init_memory();
|
|
9179
|
+
VALID_CATEGORIES = new Set(Object.keys(MEMORY_CATEGORIES));
|
|
9180
|
+
});
|
|
9181
|
+
|
|
8558
9182
|
// src/display/diff-renderer.ts
|
|
8559
9183
|
function renderDiff(diff, options = {}) {
|
|
8560
9184
|
const { maxLines, lineNumbers = true } = options;
|
|
@@ -8936,7 +9560,7 @@ var init_commands = __esm(() => {
|
|
|
8936
9560
|
|
|
8937
9561
|
// src/repl/completions.ts
|
|
8938
9562
|
import { readdirSync as readdirSync5 } from "node:fs";
|
|
8939
|
-
import { basename as basename2, dirname as dirname4, join as
|
|
9563
|
+
import { basename as basename2, dirname as dirname4, join as join18 } from "node:path";
|
|
8940
9564
|
|
|
8941
9565
|
class SlashCommandCompletion {
|
|
8942
9566
|
commands;
|
|
@@ -8991,7 +9615,7 @@ class FilePathCompletion {
|
|
|
8991
9615
|
}
|
|
8992
9616
|
findMatches(partial) {
|
|
8993
9617
|
try {
|
|
8994
|
-
const dir = partial.includes("/") ?
|
|
9618
|
+
const dir = partial.includes("/") ? join18(this.projectRoot, dirname4(partial)) : this.projectRoot;
|
|
8995
9619
|
const prefix = basename2(partial);
|
|
8996
9620
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
8997
9621
|
return entries.filter((e) => {
|
|
@@ -9027,14 +9651,14 @@ class CombinedCompletion {
|
|
|
9027
9651
|
var init_completions = () => {};
|
|
9028
9652
|
|
|
9029
9653
|
// src/repl/input-history.ts
|
|
9030
|
-
import { existsSync as
|
|
9031
|
-
import { dirname as dirname5, join as
|
|
9654
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "node:fs";
|
|
9655
|
+
import { dirname as dirname5, join as join19 } from "node:path";
|
|
9032
9656
|
|
|
9033
9657
|
class InputHistory {
|
|
9034
9658
|
entries = [];
|
|
9035
9659
|
filePath;
|
|
9036
9660
|
constructor(projectRoot) {
|
|
9037
|
-
this.filePath =
|
|
9661
|
+
this.filePath = join19(projectRoot, ".locus", "sessions", ".input-history");
|
|
9038
9662
|
this.load();
|
|
9039
9663
|
}
|
|
9040
9664
|
add(text) {
|
|
@@ -9073,9 +9697,9 @@ class InputHistory {
|
|
|
9073
9697
|
}
|
|
9074
9698
|
load() {
|
|
9075
9699
|
try {
|
|
9076
|
-
if (!
|
|
9700
|
+
if (!existsSync18(this.filePath))
|
|
9077
9701
|
return;
|
|
9078
|
-
const content =
|
|
9702
|
+
const content = readFileSync13(this.filePath, "utf-8");
|
|
9079
9703
|
this.entries = content.split(`
|
|
9080
9704
|
`).map((line) => this.unescape(line)).filter(Boolean);
|
|
9081
9705
|
} catch {}
|
|
@@ -9083,7 +9707,7 @@ class InputHistory {
|
|
|
9083
9707
|
save() {
|
|
9084
9708
|
try {
|
|
9085
9709
|
const dir = dirname5(this.filePath);
|
|
9086
|
-
if (!
|
|
9710
|
+
if (!existsSync18(dir)) {
|
|
9087
9711
|
mkdirSync12(dir, { recursive: true });
|
|
9088
9712
|
}
|
|
9089
9713
|
const content = this.entries.map((e) => this.escape(e)).join(`
|
|
@@ -9114,20 +9738,20 @@ var init_model_config = __esm(() => {
|
|
|
9114
9738
|
|
|
9115
9739
|
// src/repl/session-manager.ts
|
|
9116
9740
|
import {
|
|
9117
|
-
existsSync as
|
|
9741
|
+
existsSync as existsSync19,
|
|
9118
9742
|
mkdirSync as mkdirSync13,
|
|
9119
9743
|
readdirSync as readdirSync6,
|
|
9120
|
-
readFileSync as
|
|
9744
|
+
readFileSync as readFileSync14,
|
|
9121
9745
|
unlinkSync as unlinkSync3,
|
|
9122
9746
|
writeFileSync as writeFileSync10
|
|
9123
9747
|
} from "node:fs";
|
|
9124
|
-
import { basename as basename3, join as
|
|
9748
|
+
import { basename as basename3, join as join20 } from "node:path";
|
|
9125
9749
|
|
|
9126
9750
|
class SessionManager {
|
|
9127
9751
|
sessionsDir;
|
|
9128
9752
|
constructor(projectRoot) {
|
|
9129
|
-
this.sessionsDir =
|
|
9130
|
-
if (!
|
|
9753
|
+
this.sessionsDir = join20(projectRoot, ".locus", "sessions");
|
|
9754
|
+
if (!existsSync19(this.sessionsDir)) {
|
|
9131
9755
|
mkdirSync13(this.sessionsDir, { recursive: true });
|
|
9132
9756
|
}
|
|
9133
9757
|
}
|
|
@@ -9153,14 +9777,14 @@ class SessionManager {
|
|
|
9153
9777
|
}
|
|
9154
9778
|
isPersisted(sessionOrId) {
|
|
9155
9779
|
const sessionId = typeof sessionOrId === "string" ? sessionOrId : sessionOrId.id;
|
|
9156
|
-
return
|
|
9780
|
+
return existsSync19(this.getSessionPath(sessionId));
|
|
9157
9781
|
}
|
|
9158
9782
|
load(idOrPrefix) {
|
|
9159
9783
|
const files = this.listSessionFiles();
|
|
9160
9784
|
const exactPath = this.getSessionPath(idOrPrefix);
|
|
9161
|
-
if (
|
|
9785
|
+
if (existsSync19(exactPath)) {
|
|
9162
9786
|
try {
|
|
9163
|
-
return JSON.parse(
|
|
9787
|
+
return JSON.parse(readFileSync14(exactPath, "utf-8"));
|
|
9164
9788
|
} catch {
|
|
9165
9789
|
return null;
|
|
9166
9790
|
}
|
|
@@ -9168,7 +9792,7 @@ class SessionManager {
|
|
|
9168
9792
|
const matches = files.filter((f) => basename3(f, ".json").startsWith(idOrPrefix));
|
|
9169
9793
|
if (matches.length === 1) {
|
|
9170
9794
|
try {
|
|
9171
|
-
return JSON.parse(
|
|
9795
|
+
return JSON.parse(readFileSync14(matches[0], "utf-8"));
|
|
9172
9796
|
} catch {
|
|
9173
9797
|
return null;
|
|
9174
9798
|
}
|
|
@@ -9193,7 +9817,7 @@ class SessionManager {
|
|
|
9193
9817
|
const sessions = [];
|
|
9194
9818
|
for (const file of files) {
|
|
9195
9819
|
try {
|
|
9196
|
-
const session = JSON.parse(
|
|
9820
|
+
const session = JSON.parse(readFileSync14(file, "utf-8"));
|
|
9197
9821
|
sessions.push({
|
|
9198
9822
|
id: session.id,
|
|
9199
9823
|
created: session.created,
|
|
@@ -9208,7 +9832,7 @@ class SessionManager {
|
|
|
9208
9832
|
}
|
|
9209
9833
|
delete(sessionId) {
|
|
9210
9834
|
const path = this.getSessionPath(sessionId);
|
|
9211
|
-
if (
|
|
9835
|
+
if (existsSync19(path)) {
|
|
9212
9836
|
unlinkSync3(path);
|
|
9213
9837
|
return true;
|
|
9214
9838
|
}
|
|
@@ -9220,7 +9844,7 @@ class SessionManager {
|
|
|
9220
9844
|
let pruned = 0;
|
|
9221
9845
|
const withStats = files.map((f) => {
|
|
9222
9846
|
try {
|
|
9223
|
-
const session = JSON.parse(
|
|
9847
|
+
const session = JSON.parse(readFileSync14(f, "utf-8"));
|
|
9224
9848
|
return { path: f, updated: new Date(session.updated).getTime() };
|
|
9225
9849
|
} catch {
|
|
9226
9850
|
return { path: f, updated: 0 };
|
|
@@ -9238,7 +9862,7 @@ class SessionManager {
|
|
|
9238
9862
|
const remaining = withStats.length - pruned;
|
|
9239
9863
|
if (remaining > MAX_SESSIONS) {
|
|
9240
9864
|
const toRemove = remaining - MAX_SESSIONS;
|
|
9241
|
-
const alive = withStats.filter((e) =>
|
|
9865
|
+
const alive = withStats.filter((e) => existsSync19(e.path));
|
|
9242
9866
|
for (let i = 0;i < toRemove && i < alive.length; i++) {
|
|
9243
9867
|
try {
|
|
9244
9868
|
unlinkSync3(alive[i].path);
|
|
@@ -9253,7 +9877,7 @@ class SessionManager {
|
|
|
9253
9877
|
}
|
|
9254
9878
|
listSessionFiles() {
|
|
9255
9879
|
try {
|
|
9256
|
-
return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) =>
|
|
9880
|
+
return readdirSync6(this.sessionsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).map((f) => join20(this.sessionsDir, f));
|
|
9257
9881
|
} catch {
|
|
9258
9882
|
return [];
|
|
9259
9883
|
}
|
|
@@ -9262,7 +9886,7 @@ class SessionManager {
|
|
|
9262
9886
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
9263
9887
|
}
|
|
9264
9888
|
getSessionPath(sessionId) {
|
|
9265
|
-
return
|
|
9889
|
+
return join20(this.sessionsDir, `${sessionId}.json`);
|
|
9266
9890
|
}
|
|
9267
9891
|
}
|
|
9268
9892
|
var MAX_SESSIONS = 50, SESSION_MAX_AGE_MS;
|
|
@@ -9272,12 +9896,12 @@ var init_session_manager = __esm(() => {
|
|
|
9272
9896
|
});
|
|
9273
9897
|
|
|
9274
9898
|
// src/repl/voice.ts
|
|
9275
|
-
import { execSync as execSync11, spawn as
|
|
9276
|
-
import { existsSync as
|
|
9899
|
+
import { execSync as execSync11, spawn as spawn7 } from "node:child_process";
|
|
9900
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync14, unlinkSync as unlinkSync4 } from "node:fs";
|
|
9277
9901
|
import { cpus, homedir as homedir4, platform, tmpdir as tmpdir5 } from "node:os";
|
|
9278
|
-
import { join as
|
|
9902
|
+
import { join as join21 } from "node:path";
|
|
9279
9903
|
function getWhisperModelPath() {
|
|
9280
|
-
return
|
|
9904
|
+
return join21(WHISPER_MODELS_DIR, `ggml-${WHISPER_MODEL}.bin`);
|
|
9281
9905
|
}
|
|
9282
9906
|
function commandExists(cmd) {
|
|
9283
9907
|
try {
|
|
@@ -9295,16 +9919,16 @@ function findWhisperBinary() {
|
|
|
9295
9919
|
return name;
|
|
9296
9920
|
}
|
|
9297
9921
|
for (const name of candidates) {
|
|
9298
|
-
const fullPath =
|
|
9299
|
-
if (
|
|
9922
|
+
const fullPath = join21(LOCUS_BIN_DIR, name);
|
|
9923
|
+
if (existsSync20(fullPath))
|
|
9300
9924
|
return fullPath;
|
|
9301
9925
|
}
|
|
9302
9926
|
if (platform() === "darwin") {
|
|
9303
9927
|
const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
9304
9928
|
for (const dir of brewDirs) {
|
|
9305
9929
|
for (const name of candidates) {
|
|
9306
|
-
const fullPath =
|
|
9307
|
-
if (
|
|
9930
|
+
const fullPath = join21(dir, name);
|
|
9931
|
+
if (existsSync20(fullPath))
|
|
9308
9932
|
return fullPath;
|
|
9309
9933
|
}
|
|
9310
9934
|
}
|
|
@@ -9319,11 +9943,11 @@ function findSoxRecBinary() {
|
|
|
9319
9943
|
if (platform() === "darwin") {
|
|
9320
9944
|
const brewDirs = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
9321
9945
|
for (const dir of brewDirs) {
|
|
9322
|
-
const recPath =
|
|
9323
|
-
if (
|
|
9946
|
+
const recPath = join21(dir, "rec");
|
|
9947
|
+
if (existsSync20(recPath))
|
|
9324
9948
|
return recPath;
|
|
9325
|
-
const soxPath =
|
|
9326
|
-
if (
|
|
9949
|
+
const soxPath = join21(dir, "sox");
|
|
9950
|
+
if (existsSync20(soxPath))
|
|
9327
9951
|
return soxPath;
|
|
9328
9952
|
}
|
|
9329
9953
|
}
|
|
@@ -9332,7 +9956,7 @@ function findSoxRecBinary() {
|
|
|
9332
9956
|
function checkDependencies() {
|
|
9333
9957
|
const soxBinary = findSoxRecBinary();
|
|
9334
9958
|
const whisperBinary = findWhisperBinary();
|
|
9335
|
-
const modelDownloaded =
|
|
9959
|
+
const modelDownloaded = existsSync20(getWhisperModelPath());
|
|
9336
9960
|
return {
|
|
9337
9961
|
sox: soxBinary !== null,
|
|
9338
9962
|
whisper: whisperBinary !== null,
|
|
@@ -9486,7 +10110,7 @@ function ensureBuildDeps(pm) {
|
|
|
9486
10110
|
}
|
|
9487
10111
|
function buildWhisperFromSource(pm) {
|
|
9488
10112
|
const out = process.stderr;
|
|
9489
|
-
const buildDir =
|
|
10113
|
+
const buildDir = join21(tmpdir5(), `locus-whisper-build-${process.pid}`);
|
|
9490
10114
|
if (!ensureBuildDeps(pm)) {
|
|
9491
10115
|
out.write(` ${red2("✗")} Could not install build tools (cmake, g++, git).
|
|
9492
10116
|
`);
|
|
@@ -9497,8 +10121,8 @@ function buildWhisperFromSource(pm) {
|
|
|
9497
10121
|
mkdirSync14(LOCUS_BIN_DIR, { recursive: true });
|
|
9498
10122
|
out.write(` ${dim2("Cloning whisper.cpp...")}
|
|
9499
10123
|
`);
|
|
9500
|
-
execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${
|
|
9501
|
-
const srcDir =
|
|
10124
|
+
execSync11(`git clone --depth 1 https://github.com/ggerganov/whisper.cpp.git "${join21(buildDir, "whisper.cpp")}"`, { stdio: ["pipe", "pipe", "pipe"], timeout: 120000 });
|
|
10125
|
+
const srcDir = join21(buildDir, "whisper.cpp");
|
|
9502
10126
|
const numCpus = cpus().length || 2;
|
|
9503
10127
|
out.write(` ${dim2("Building whisper.cpp (this may take a few minutes)...")}
|
|
9504
10128
|
`);
|
|
@@ -9512,13 +10136,13 @@ function buildWhisperFromSource(pm) {
|
|
|
9512
10136
|
stdio: ["pipe", "pipe", "pipe"],
|
|
9513
10137
|
timeout: 600000
|
|
9514
10138
|
});
|
|
9515
|
-
const destPath =
|
|
10139
|
+
const destPath = join21(LOCUS_BIN_DIR, "whisper-cli");
|
|
9516
10140
|
const binaryCandidates = [
|
|
9517
|
-
|
|
9518
|
-
|
|
10141
|
+
join21(srcDir, "build", "bin", "whisper-cli"),
|
|
10142
|
+
join21(srcDir, "build", "bin", "main")
|
|
9519
10143
|
];
|
|
9520
10144
|
for (const candidate of binaryCandidates) {
|
|
9521
|
-
if (
|
|
10145
|
+
if (existsSync20(candidate)) {
|
|
9522
10146
|
execSync11(`cp "${candidate}" "${destPath}" && chmod +x "${destPath}"`, {
|
|
9523
10147
|
stdio: "pipe"
|
|
9524
10148
|
});
|
|
@@ -9597,7 +10221,7 @@ ${bold2("Installing voice dependencies...")}
|
|
|
9597
10221
|
}
|
|
9598
10222
|
function downloadModel() {
|
|
9599
10223
|
const modelPath = getWhisperModelPath();
|
|
9600
|
-
if (
|
|
10224
|
+
if (existsSync20(modelPath))
|
|
9601
10225
|
return true;
|
|
9602
10226
|
mkdirSync14(WHISPER_MODELS_DIR, { recursive: true });
|
|
9603
10227
|
const url = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${WHISPER_MODEL === "base.en" ? "ggml-base.en.bin" : `ggml-${WHISPER_MODEL}.bin`}`;
|
|
@@ -9644,7 +10268,7 @@ class VoiceController {
|
|
|
9644
10268
|
onStateChange;
|
|
9645
10269
|
constructor(options) {
|
|
9646
10270
|
this.onStateChange = options.onStateChange;
|
|
9647
|
-
this.tempFile =
|
|
10271
|
+
this.tempFile = join21(tmpdir5(), `locus-voice-${process.pid}.wav`);
|
|
9648
10272
|
this.deps = checkDependencies();
|
|
9649
10273
|
}
|
|
9650
10274
|
getState() {
|
|
@@ -9680,7 +10304,7 @@ class VoiceController {
|
|
|
9680
10304
|
return false;
|
|
9681
10305
|
}
|
|
9682
10306
|
const spawnArgs = binary === "rec" ? args : ["-d", ...args];
|
|
9683
|
-
this.recordProcess =
|
|
10307
|
+
this.recordProcess = spawn7(binary, spawnArgs, {
|
|
9684
10308
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9685
10309
|
});
|
|
9686
10310
|
this.recordProcess.on("error", (err) => {
|
|
@@ -9712,7 +10336,7 @@ ${red2("✗")} Recording failed: ${err.message}\r
|
|
|
9712
10336
|
this.recordProcess = null;
|
|
9713
10337
|
this.setState("idle");
|
|
9714
10338
|
await sleep2(200);
|
|
9715
|
-
if (!
|
|
10339
|
+
if (!existsSync20(this.tempFile)) {
|
|
9716
10340
|
return null;
|
|
9717
10341
|
}
|
|
9718
10342
|
try {
|
|
@@ -9747,7 +10371,7 @@ ${red2("✗")} Recording failed: ${err.message}\r
|
|
|
9747
10371
|
"--language",
|
|
9748
10372
|
WHISPER_MODEL.endsWith(".en") ? "en" : "auto"
|
|
9749
10373
|
];
|
|
9750
|
-
const proc =
|
|
10374
|
+
const proc = spawn7(binary, args, {
|
|
9751
10375
|
stdio: ["pipe", "pipe", "pipe"]
|
|
9752
10376
|
});
|
|
9753
10377
|
let stdout = "";
|
|
@@ -9803,8 +10427,8 @@ function sleep2(ms) {
|
|
|
9803
10427
|
var WHISPER_MODEL = "base.en", WHISPER_MODELS_DIR, LOCUS_BIN_DIR;
|
|
9804
10428
|
var init_voice = __esm(() => {
|
|
9805
10429
|
init_terminal();
|
|
9806
|
-
WHISPER_MODELS_DIR =
|
|
9807
|
-
LOCUS_BIN_DIR =
|
|
10430
|
+
WHISPER_MODELS_DIR = join21(homedir4(), ".locus", "whisper-models");
|
|
10431
|
+
LOCUS_BIN_DIR = join21(homedir4(), ".locus", "bin");
|
|
9808
10432
|
});
|
|
9809
10433
|
|
|
9810
10434
|
// src/repl/repl.ts
|
|
@@ -9842,6 +10466,16 @@ async function startRepl(options) {
|
|
|
9842
10466
|
}
|
|
9843
10467
|
if (options.prompt) {
|
|
9844
10468
|
await executeOneShotPrompt(options.prompt, session, sessionManager, options);
|
|
10469
|
+
if (session.messages.length >= 2) {
|
|
10470
|
+
const log = getLogger();
|
|
10471
|
+
const transcript = prepareTranscript(session.messages);
|
|
10472
|
+
captureMemoryFromSession(projectRoot, transcript, {
|
|
10473
|
+
model: config.ai?.model
|
|
10474
|
+
}).then((result) => {
|
|
10475
|
+
if (result.captured > 0)
|
|
10476
|
+
log.info(`Captured ${result.captured} memory entries`);
|
|
10477
|
+
}).catch(() => {});
|
|
10478
|
+
}
|
|
9845
10479
|
return;
|
|
9846
10480
|
}
|
|
9847
10481
|
if (!process.stdin.isTTY) {
|
|
@@ -9906,6 +10540,7 @@ async function runInteractiveRepl(session, sessionManager, options) {
|
|
|
9906
10540
|
});
|
|
9907
10541
|
printWelcome(session);
|
|
9908
10542
|
let shouldExit = false;
|
|
10543
|
+
let wasInterrupted = false;
|
|
9909
10544
|
let currentProvider = inferProviderFromModel(config.ai.model) || config.ai.provider;
|
|
9910
10545
|
let currentModel = config.ai.model;
|
|
9911
10546
|
let verbose = true;
|
|
@@ -10021,6 +10656,7 @@ ${red2("✗")} ${msg}
|
|
|
10021
10656
|
break;
|
|
10022
10657
|
}
|
|
10023
10658
|
case "interrupt":
|
|
10659
|
+
wasInterrupted = true;
|
|
10024
10660
|
shouldExit = true;
|
|
10025
10661
|
break;
|
|
10026
10662
|
case "exit":
|
|
@@ -10031,6 +10667,16 @@ ${red2("✗")} ${msg}
|
|
|
10031
10667
|
}
|
|
10032
10668
|
}
|
|
10033
10669
|
voice.cancel();
|
|
10670
|
+
if (!wasInterrupted && session.messages.length >= 2) {
|
|
10671
|
+
const log = getLogger();
|
|
10672
|
+
const transcript = prepareTranscript(session.messages);
|
|
10673
|
+
captureMemoryFromSession(projectRoot, transcript, {
|
|
10674
|
+
model: config.ai?.model
|
|
10675
|
+
}).then((result) => {
|
|
10676
|
+
if (result.captured > 0)
|
|
10677
|
+
log.info(`Captured ${result.captured} memory entries`);
|
|
10678
|
+
}).catch(() => {});
|
|
10679
|
+
}
|
|
10034
10680
|
const shouldPersistOnExit = session.messages.length > 0 || sessionManager.isPersisted(session);
|
|
10035
10681
|
if (shouldPersistOnExit) {
|
|
10036
10682
|
sessionManager.save(session);
|
|
@@ -10107,6 +10753,8 @@ var init_repl = __esm(() => {
|
|
|
10107
10753
|
init_run_ai();
|
|
10108
10754
|
init_runner();
|
|
10109
10755
|
init_ai_models();
|
|
10756
|
+
init_logger();
|
|
10757
|
+
init_memory_capture();
|
|
10110
10758
|
init_prompt_builder();
|
|
10111
10759
|
init_sandbox();
|
|
10112
10760
|
init_terminal();
|
|
@@ -10316,8 +10964,8 @@ var init_exec = __esm(() => {
|
|
|
10316
10964
|
|
|
10317
10965
|
// src/core/submodule.ts
|
|
10318
10966
|
import { execSync as execSync13 } from "node:child_process";
|
|
10319
|
-
import { existsSync as
|
|
10320
|
-
import { join as
|
|
10967
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
10968
|
+
import { join as join22 } from "node:path";
|
|
10321
10969
|
function git2(args, cwd) {
|
|
10322
10970
|
return execSync13(`git ${args}`, {
|
|
10323
10971
|
cwd,
|
|
@@ -10333,7 +10981,7 @@ function gitSafe(args, cwd) {
|
|
|
10333
10981
|
}
|
|
10334
10982
|
}
|
|
10335
10983
|
function hasSubmodules(cwd) {
|
|
10336
|
-
return
|
|
10984
|
+
return existsSync21(join22(cwd, ".gitmodules"));
|
|
10337
10985
|
}
|
|
10338
10986
|
function listSubmodules(cwd) {
|
|
10339
10987
|
if (!hasSubmodules(cwd))
|
|
@@ -10353,7 +11001,7 @@ function listSubmodules(cwd) {
|
|
|
10353
11001
|
continue;
|
|
10354
11002
|
submodules.push({
|
|
10355
11003
|
path,
|
|
10356
|
-
absolutePath:
|
|
11004
|
+
absolutePath: join22(cwd, path),
|
|
10357
11005
|
dirty
|
|
10358
11006
|
});
|
|
10359
11007
|
}
|
|
@@ -10366,7 +11014,7 @@ function getDirtySubmodules(cwd) {
|
|
|
10366
11014
|
const submodules = listSubmodules(cwd);
|
|
10367
11015
|
const dirty = [];
|
|
10368
11016
|
for (const sub of submodules) {
|
|
10369
|
-
if (!
|
|
11017
|
+
if (!existsSync21(sub.absolutePath))
|
|
10370
11018
|
continue;
|
|
10371
11019
|
const status = gitSafe("status --porcelain", sub.absolutePath);
|
|
10372
11020
|
if (status && status.trim().length > 0) {
|
|
@@ -10453,7 +11101,7 @@ function pushSubmoduleBranches(cwd) {
|
|
|
10453
11101
|
const log = getLogger();
|
|
10454
11102
|
const submodules = listSubmodules(cwd);
|
|
10455
11103
|
for (const sub of submodules) {
|
|
10456
|
-
if (!
|
|
11104
|
+
if (!existsSync21(sub.absolutePath))
|
|
10457
11105
|
continue;
|
|
10458
11106
|
const branch = gitSafe("rev-parse --abbrev-ref HEAD", sub.absolutePath)?.trim();
|
|
10459
11107
|
if (!branch || branch === "HEAD")
|
|
@@ -10605,6 +11253,19 @@ ${summary}
|
|
|
10605
11253
|
Duration: ${timer.formatted()}${prNumber ? `
|
|
10606
11254
|
PR: #${prNumber}` : ""}`, { cwd: projectRoot });
|
|
10607
11255
|
} catch {}
|
|
11256
|
+
const transcript = prepareTranscript([
|
|
11257
|
+
{
|
|
11258
|
+
role: "user",
|
|
11259
|
+
content: `Issue #${issueNumber}: ${issue.title}
|
|
11260
|
+
|
|
11261
|
+
${issue.body}`
|
|
11262
|
+
},
|
|
11263
|
+
{ role: "assistant", content: output }
|
|
11264
|
+
]);
|
|
11265
|
+
captureMemoryFromSession(projectRoot, transcript, { model: config.ai?.model }).then((result) => {
|
|
11266
|
+
if (result.captured > 0)
|
|
11267
|
+
log.info(`Captured ${result.captured} memory entries`);
|
|
11268
|
+
}).catch(() => {});
|
|
10608
11269
|
return {
|
|
10609
11270
|
issueNumber,
|
|
10610
11271
|
success: true,
|
|
@@ -10786,6 +11447,7 @@ var init_agent = __esm(() => {
|
|
|
10786
11447
|
init_config();
|
|
10787
11448
|
init_github();
|
|
10788
11449
|
init_logger();
|
|
11450
|
+
init_memory_capture();
|
|
10789
11451
|
init_prompt_builder();
|
|
10790
11452
|
init_sandbox();
|
|
10791
11453
|
init_submodule();
|
|
@@ -10923,15 +11585,15 @@ var init_conflict = __esm(() => {
|
|
|
10923
11585
|
|
|
10924
11586
|
// src/core/run-state.ts
|
|
10925
11587
|
import {
|
|
10926
|
-
existsSync as
|
|
11588
|
+
existsSync as existsSync22,
|
|
10927
11589
|
mkdirSync as mkdirSync15,
|
|
10928
|
-
readFileSync as
|
|
11590
|
+
readFileSync as readFileSync15,
|
|
10929
11591
|
unlinkSync as unlinkSync5,
|
|
10930
11592
|
writeFileSync as writeFileSync11
|
|
10931
11593
|
} from "node:fs";
|
|
10932
|
-
import { dirname as dirname6, join as
|
|
11594
|
+
import { dirname as dirname6, join as join23 } from "node:path";
|
|
10933
11595
|
function getRunStateDir(projectRoot) {
|
|
10934
|
-
return
|
|
11596
|
+
return join23(projectRoot, ".locus", "run-state");
|
|
10935
11597
|
}
|
|
10936
11598
|
function sprintSlug(name) {
|
|
10937
11599
|
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -10939,16 +11601,16 @@ function sprintSlug(name) {
|
|
|
10939
11601
|
function getRunStatePath(projectRoot, sprintName) {
|
|
10940
11602
|
const dir = getRunStateDir(projectRoot);
|
|
10941
11603
|
if (sprintName) {
|
|
10942
|
-
return
|
|
11604
|
+
return join23(dir, `${sprintSlug(sprintName)}.json`);
|
|
10943
11605
|
}
|
|
10944
|
-
return
|
|
11606
|
+
return join23(dir, "_parallel.json");
|
|
10945
11607
|
}
|
|
10946
11608
|
function loadRunState(projectRoot, sprintName) {
|
|
10947
11609
|
const path = getRunStatePath(projectRoot, sprintName);
|
|
10948
|
-
if (!
|
|
11610
|
+
if (!existsSync22(path))
|
|
10949
11611
|
return null;
|
|
10950
11612
|
try {
|
|
10951
|
-
return JSON.parse(
|
|
11613
|
+
return JSON.parse(readFileSync15(path, "utf-8"));
|
|
10952
11614
|
} catch {
|
|
10953
11615
|
getLogger().warn("Corrupted run state file, ignoring");
|
|
10954
11616
|
return null;
|
|
@@ -10957,7 +11619,7 @@ function loadRunState(projectRoot, sprintName) {
|
|
|
10957
11619
|
function saveRunState(projectRoot, state) {
|
|
10958
11620
|
const path = getRunStatePath(projectRoot, state.sprint);
|
|
10959
11621
|
const dir = dirname6(path);
|
|
10960
|
-
if (!
|
|
11622
|
+
if (!existsSync22(dir)) {
|
|
10961
11623
|
mkdirSync15(dir, { recursive: true });
|
|
10962
11624
|
}
|
|
10963
11625
|
writeFileSync11(path, `${JSON.stringify(state, null, 2)}
|
|
@@ -10965,7 +11627,7 @@ function saveRunState(projectRoot, state) {
|
|
|
10965
11627
|
}
|
|
10966
11628
|
function clearRunState(projectRoot, sprintName) {
|
|
10967
11629
|
const path = getRunStatePath(projectRoot, sprintName);
|
|
10968
|
-
if (
|
|
11630
|
+
if (existsSync22(path)) {
|
|
10969
11631
|
unlinkSync5(path);
|
|
10970
11632
|
}
|
|
10971
11633
|
}
|
|
@@ -11123,23 +11785,23 @@ __export(exports_worktree, {
|
|
|
11123
11785
|
import { execSync as execSync16 } from "node:child_process";
|
|
11124
11786
|
import {
|
|
11125
11787
|
cpSync as cpSync2,
|
|
11126
|
-
existsSync as
|
|
11788
|
+
existsSync as existsSync23,
|
|
11127
11789
|
mkdirSync as mkdirSync16,
|
|
11128
11790
|
readdirSync as readdirSync7,
|
|
11129
11791
|
realpathSync,
|
|
11130
11792
|
statSync as statSync4
|
|
11131
11793
|
} from "node:fs";
|
|
11132
|
-
import { join as
|
|
11794
|
+
import { join as join24 } from "node:path";
|
|
11133
11795
|
function copyLocusDir(projectRoot, worktreePath) {
|
|
11134
|
-
const srcLocus =
|
|
11135
|
-
if (!
|
|
11796
|
+
const srcLocus = join24(projectRoot, ".locus");
|
|
11797
|
+
if (!existsSync23(srcLocus))
|
|
11136
11798
|
return;
|
|
11137
|
-
const destLocus =
|
|
11799
|
+
const destLocus = join24(worktreePath, ".locus");
|
|
11138
11800
|
mkdirSync16(destLocus, { recursive: true });
|
|
11139
11801
|
for (const entry of readdirSync7(srcLocus)) {
|
|
11140
11802
|
if (entry === "worktrees")
|
|
11141
11803
|
continue;
|
|
11142
|
-
cpSync2(
|
|
11804
|
+
cpSync2(join24(srcLocus, entry), join24(destLocus, entry), { recursive: true });
|
|
11143
11805
|
}
|
|
11144
11806
|
}
|
|
11145
11807
|
function git4(args, cwd) {
|
|
@@ -11157,13 +11819,13 @@ function gitSafe3(args, cwd) {
|
|
|
11157
11819
|
}
|
|
11158
11820
|
}
|
|
11159
11821
|
function getWorktreeDir(projectRoot) {
|
|
11160
|
-
return
|
|
11822
|
+
return join24(projectRoot, ".locus", "worktrees");
|
|
11161
11823
|
}
|
|
11162
11824
|
function getWorktreePath(projectRoot, issueNumber) {
|
|
11163
|
-
return
|
|
11825
|
+
return join24(getWorktreeDir(projectRoot), `issue-${issueNumber}`);
|
|
11164
11826
|
}
|
|
11165
11827
|
function getSprintWorktreePath(projectRoot, sprintSlug2) {
|
|
11166
|
-
return
|
|
11828
|
+
return join24(getWorktreeDir(projectRoot), `sprint-${sprintSlug2}`);
|
|
11167
11829
|
}
|
|
11168
11830
|
function generateBranchName(issueNumber) {
|
|
11169
11831
|
const randomSuffix = Math.random().toString(36).slice(2, 8);
|
|
@@ -11176,7 +11838,7 @@ function createSprintWorktree(projectRoot, sprintName, baseBranch) {
|
|
|
11176
11838
|
const log = getLogger();
|
|
11177
11839
|
const slug = sprintSlug2(sprintName);
|
|
11178
11840
|
const worktreePath = getSprintWorktreePath(projectRoot, slug);
|
|
11179
|
-
if (
|
|
11841
|
+
if (existsSync23(worktreePath)) {
|
|
11180
11842
|
log.verbose(`Sprint worktree already exists for "${sprintName}"`);
|
|
11181
11843
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/sprint-${slug}`;
|
|
11182
11844
|
return { path: worktreePath, branch: existingBranch };
|
|
@@ -11197,7 +11859,7 @@ function removeSprintWorktree(projectRoot, sprintName) {
|
|
|
11197
11859
|
const log = getLogger();
|
|
11198
11860
|
const slug = sprintSlug2(sprintName);
|
|
11199
11861
|
const worktreePath = getSprintWorktreePath(projectRoot, slug);
|
|
11200
|
-
if (!
|
|
11862
|
+
if (!existsSync23(worktreePath)) {
|
|
11201
11863
|
log.verbose(`Sprint worktree for "${sprintName}" does not exist`);
|
|
11202
11864
|
return;
|
|
11203
11865
|
}
|
|
@@ -11227,7 +11889,7 @@ function getWorktreeBranch(worktreePath) {
|
|
|
11227
11889
|
function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
11228
11890
|
const log = getLogger();
|
|
11229
11891
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
11230
|
-
if (
|
|
11892
|
+
if (existsSync23(worktreePath)) {
|
|
11231
11893
|
log.verbose(`Worktree already exists for issue #${issueNumber}`);
|
|
11232
11894
|
const existingBranch = getWorktreeBranch(worktreePath) ?? `locus/issue-${issueNumber}`;
|
|
11233
11895
|
return {
|
|
@@ -11256,7 +11918,7 @@ function createWorktree(projectRoot, issueNumber, baseBranch) {
|
|
|
11256
11918
|
function removeWorktree(projectRoot, issueNumber) {
|
|
11257
11919
|
const log = getLogger();
|
|
11258
11920
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
11259
|
-
if (!
|
|
11921
|
+
if (!existsSync23(worktreePath)) {
|
|
11260
11922
|
log.verbose(`Worktree for issue #${issueNumber} does not exist`);
|
|
11261
11923
|
return;
|
|
11262
11924
|
}
|
|
@@ -11275,7 +11937,7 @@ function removeWorktree(projectRoot, issueNumber) {
|
|
|
11275
11937
|
function listWorktrees(projectRoot) {
|
|
11276
11938
|
const log = getLogger();
|
|
11277
11939
|
const worktreeDir = getWorktreeDir(projectRoot);
|
|
11278
|
-
if (!
|
|
11940
|
+
if (!existsSync23(worktreeDir)) {
|
|
11279
11941
|
return [];
|
|
11280
11942
|
}
|
|
11281
11943
|
const entries = readdirSync7(worktreeDir).filter((entry) => entry.startsWith("issue-"));
|
|
@@ -11295,7 +11957,7 @@ function listWorktrees(projectRoot) {
|
|
|
11295
11957
|
if (!match)
|
|
11296
11958
|
continue;
|
|
11297
11959
|
const issueNumber = Number.parseInt(match[1], 10);
|
|
11298
|
-
const path =
|
|
11960
|
+
const path = join24(worktreeDir, entry);
|
|
11299
11961
|
const branch = getWorktreeBranch(path) ?? `locus/issue-${issueNumber}`;
|
|
11300
11962
|
let resolvedPath;
|
|
11301
11963
|
try {
|
|
@@ -11335,7 +11997,7 @@ function cleanupStaleWorktrees(projectRoot) {
|
|
|
11335
11997
|
}
|
|
11336
11998
|
function pushWorktreeBranch(projectRoot, issueNumber) {
|
|
11337
11999
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
11338
|
-
if (!
|
|
12000
|
+
if (!existsSync23(worktreePath)) {
|
|
11339
12001
|
throw new Error(`Worktree for issue #${issueNumber} does not exist`);
|
|
11340
12002
|
}
|
|
11341
12003
|
const branch = getWorktreeBranch(worktreePath);
|
|
@@ -11347,18 +12009,18 @@ function pushWorktreeBranch(projectRoot, issueNumber) {
|
|
|
11347
12009
|
}
|
|
11348
12010
|
function hasWorktreeChanges(projectRoot, issueNumber) {
|
|
11349
12011
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
11350
|
-
if (!
|
|
12012
|
+
if (!existsSync23(worktreePath))
|
|
11351
12013
|
return false;
|
|
11352
12014
|
const status = gitSafe3("status --porcelain", worktreePath);
|
|
11353
12015
|
return status !== null && status.trim().length > 0;
|
|
11354
12016
|
}
|
|
11355
12017
|
function getWorktreeAge(projectRoot, issueNumber) {
|
|
11356
12018
|
const worktreePath = getWorktreePath(projectRoot, issueNumber);
|
|
11357
|
-
if (!
|
|
12019
|
+
if (!existsSync23(worktreePath))
|
|
11358
12020
|
return 0;
|
|
11359
12021
|
try {
|
|
11360
|
-
const
|
|
11361
|
-
return Date.now() -
|
|
12022
|
+
const stat2 = statSync4(worktreePath);
|
|
12023
|
+
return Date.now() - stat2.ctimeMs;
|
|
11362
12024
|
} catch {
|
|
11363
12025
|
return 0;
|
|
11364
12026
|
}
|
|
@@ -11374,8 +12036,8 @@ __export(exports_run, {
|
|
|
11374
12036
|
runCommand: () => runCommand
|
|
11375
12037
|
});
|
|
11376
12038
|
import { execSync as execSync17 } from "node:child_process";
|
|
11377
|
-
import { existsSync as
|
|
11378
|
-
import { join as
|
|
12039
|
+
import { existsSync as existsSync24 } from "node:fs";
|
|
12040
|
+
import { join as join25 } from "node:path";
|
|
11379
12041
|
function resolveExecutionContext(config, modelOverride) {
|
|
11380
12042
|
const model = modelOverride ?? config.ai.model;
|
|
11381
12043
|
const provider = inferProviderFromModel(model) ?? config.ai.provider;
|
|
@@ -11933,8 +12595,8 @@ async function handleResume(projectRoot, config, sandboxed, flags) {
|
|
|
11933
12595
|
const sprintsToResume = [];
|
|
11934
12596
|
try {
|
|
11935
12597
|
const { readdirSync: readdirSync8 } = await import("node:fs");
|
|
11936
|
-
const runStateDir =
|
|
11937
|
-
if (
|
|
12598
|
+
const runStateDir = join25(projectRoot, ".locus", "run-state");
|
|
12599
|
+
if (existsSync24(runStateDir)) {
|
|
11938
12600
|
const files = readdirSync8(runStateDir).filter((f) => f.endsWith(".json"));
|
|
11939
12601
|
for (const file of files) {
|
|
11940
12602
|
const sprintName = file === "_parallel.json" ? undefined : file.replace(/\.json$/, "");
|
|
@@ -11978,9 +12640,9 @@ ${bold2("Resuming")} ${state.type} run ${dim2(state.runId)}${state.sprint ? ` ($
|
|
|
11978
12640
|
let workDir = projectRoot;
|
|
11979
12641
|
if (state.type === "sprint" && state.sprint) {
|
|
11980
12642
|
const { getSprintWorktreePath: getSprintWorktreePath2, sprintSlug: sprintSlug3 } = await Promise.resolve().then(() => (init_worktree(), exports_worktree));
|
|
11981
|
-
const { existsSync:
|
|
12643
|
+
const { existsSync: existsSync25 } = await import("node:fs");
|
|
11982
12644
|
const wtPath = getSprintWorktreePath2(projectRoot, sprintSlug3(state.sprint));
|
|
11983
|
-
if (
|
|
12645
|
+
if (existsSync25(wtPath)) {
|
|
11984
12646
|
workDir = wtPath;
|
|
11985
12647
|
} else if (state.branch) {
|
|
11986
12648
|
try {
|
|
@@ -12210,8 +12872,8 @@ __export(exports_status, {
|
|
|
12210
12872
|
statusCommand: () => statusCommand
|
|
12211
12873
|
});
|
|
12212
12874
|
import { execSync as execSync18 } from "node:child_process";
|
|
12213
|
-
import { existsSync as
|
|
12214
|
-
import { dirname as dirname7, join as
|
|
12875
|
+
import { existsSync as existsSync25 } from "node:fs";
|
|
12876
|
+
import { dirname as dirname7, join as join26 } from "node:path";
|
|
12215
12877
|
async function statusCommand(projectRoot) {
|
|
12216
12878
|
const config = loadConfig(projectRoot);
|
|
12217
12879
|
const spinner = new Spinner;
|
|
@@ -12343,13 +13005,13 @@ ${drawBox(lines, { title: "Locus Status" })}
|
|
|
12343
13005
|
`);
|
|
12344
13006
|
}
|
|
12345
13007
|
function getPm2Bin() {
|
|
12346
|
-
const pkgsBin =
|
|
12347
|
-
if (
|
|
13008
|
+
const pkgsBin = join26(getPackagesDir(), "node_modules", ".bin", "pm2");
|
|
13009
|
+
if (existsSync25(pkgsBin))
|
|
12348
13010
|
return pkgsBin;
|
|
12349
13011
|
let dir = process.cwd();
|
|
12350
13012
|
while (dir !== dirname7(dir)) {
|
|
12351
|
-
const candidate =
|
|
12352
|
-
if (
|
|
13013
|
+
const candidate = join26(dir, "node_modules", ".bin", "pm2");
|
|
13014
|
+
if (existsSync25(candidate))
|
|
12353
13015
|
return candidate;
|
|
12354
13016
|
dir = dirname7(dir);
|
|
12355
13017
|
}
|
|
@@ -12545,13 +13207,13 @@ __export(exports_plan, {
|
|
|
12545
13207
|
parsePlanArgs: () => parsePlanArgs
|
|
12546
13208
|
});
|
|
12547
13209
|
import {
|
|
12548
|
-
existsSync as
|
|
13210
|
+
existsSync as existsSync26,
|
|
12549
13211
|
mkdirSync as mkdirSync17,
|
|
12550
13212
|
readdirSync as readdirSync8,
|
|
12551
|
-
readFileSync as
|
|
13213
|
+
readFileSync as readFileSync16,
|
|
12552
13214
|
writeFileSync as writeFileSync12
|
|
12553
13215
|
} from "node:fs";
|
|
12554
|
-
import { join as
|
|
13216
|
+
import { join as join27 } from "node:path";
|
|
12555
13217
|
function printHelp2() {
|
|
12556
13218
|
process.stderr.write(`
|
|
12557
13219
|
${bold2("locus plan")} — AI-powered sprint planning
|
|
@@ -12583,11 +13245,11 @@ function normalizeSprintName(name) {
|
|
|
12583
13245
|
return name.trim().toLowerCase();
|
|
12584
13246
|
}
|
|
12585
13247
|
function getPlansDir(projectRoot) {
|
|
12586
|
-
return
|
|
13248
|
+
return join27(projectRoot, ".locus", "plans");
|
|
12587
13249
|
}
|
|
12588
13250
|
function ensurePlansDir(projectRoot) {
|
|
12589
13251
|
const dir = getPlansDir(projectRoot);
|
|
12590
|
-
if (!
|
|
13252
|
+
if (!existsSync26(dir)) {
|
|
12591
13253
|
mkdirSync17(dir, { recursive: true });
|
|
12592
13254
|
}
|
|
12593
13255
|
return dir;
|
|
@@ -12597,14 +13259,14 @@ function generateId() {
|
|
|
12597
13259
|
}
|
|
12598
13260
|
function loadPlanFile(projectRoot, id) {
|
|
12599
13261
|
const dir = getPlansDir(projectRoot);
|
|
12600
|
-
if (!
|
|
13262
|
+
if (!existsSync26(dir))
|
|
12601
13263
|
return null;
|
|
12602
13264
|
const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
|
|
12603
13265
|
const match = files.find((f) => f.startsWith(id));
|
|
12604
13266
|
if (!match)
|
|
12605
13267
|
return null;
|
|
12606
13268
|
try {
|
|
12607
|
-
const content =
|
|
13269
|
+
const content = readFileSync16(join27(dir, match), "utf-8");
|
|
12608
13270
|
return JSON.parse(content);
|
|
12609
13271
|
} catch {
|
|
12610
13272
|
return null;
|
|
@@ -12671,7 +13333,7 @@ async function planCommand(projectRoot, args, flags = {}) {
|
|
|
12671
13333
|
}
|
|
12672
13334
|
function handleListPlans(projectRoot) {
|
|
12673
13335
|
const dir = getPlansDir(projectRoot);
|
|
12674
|
-
if (!
|
|
13336
|
+
if (!existsSync26(dir)) {
|
|
12675
13337
|
process.stderr.write(`${dim2("No saved plans yet.")}
|
|
12676
13338
|
`);
|
|
12677
13339
|
return;
|
|
@@ -12689,7 +13351,7 @@ ${bold2("Saved Plans:")}
|
|
|
12689
13351
|
for (const file of files) {
|
|
12690
13352
|
const id = file.replace(".json", "");
|
|
12691
13353
|
try {
|
|
12692
|
-
const content =
|
|
13354
|
+
const content = readFileSync16(join27(dir, file), "utf-8");
|
|
12693
13355
|
const plan = JSON.parse(content);
|
|
12694
13356
|
const date = plan.createdAt ? plan.createdAt.slice(0, 10) : "";
|
|
12695
13357
|
const issueCount = Array.isArray(plan.issues) ? plan.issues.length : 0;
|
|
@@ -12757,7 +13419,7 @@ async function handleRefinePlan(projectRoot, id, feedback, flags) {
|
|
|
12757
13419
|
return;
|
|
12758
13420
|
}
|
|
12759
13421
|
const config = loadConfig(projectRoot);
|
|
12760
|
-
const planPath =
|
|
13422
|
+
const planPath = join27(getPlansDir(projectRoot), `${plan.id}.json`);
|
|
12761
13423
|
const planPathRelative = `.locus/plans/${plan.id}.json`;
|
|
12762
13424
|
process.stderr.write(`
|
|
12763
13425
|
${bold2("Refining plan:")} ${cyan2(plan.directive)}
|
|
@@ -12796,7 +13458,7 @@ ${red2("✗")} Refinement failed: ${aiResult.error}
|
|
|
12796
13458
|
`);
|
|
12797
13459
|
return;
|
|
12798
13460
|
}
|
|
12799
|
-
if (!
|
|
13461
|
+
if (!existsSync26(planPath)) {
|
|
12800
13462
|
process.stderr.write(`
|
|
12801
13463
|
${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
|
|
12802
13464
|
`);
|
|
@@ -12804,7 +13466,7 @@ ${yellow2("⚠")} Plan file was not found at ${bold2(planPathRelative)}.
|
|
|
12804
13466
|
}
|
|
12805
13467
|
let updatedPlan;
|
|
12806
13468
|
try {
|
|
12807
|
-
const content =
|
|
13469
|
+
const content = readFileSync16(planPath, "utf-8");
|
|
12808
13470
|
updatedPlan = JSON.parse(content);
|
|
12809
13471
|
} catch {
|
|
12810
13472
|
process.stderr.write(`
|
|
@@ -12894,7 +13556,7 @@ ${bold2("Approving plan:")}
|
|
|
12894
13556
|
async function handleAIPlan(projectRoot, config, directive, sprintName, flags) {
|
|
12895
13557
|
const id = generateId();
|
|
12896
13558
|
const plansDir = ensurePlansDir(projectRoot);
|
|
12897
|
-
const planPath =
|
|
13559
|
+
const planPath = join27(plansDir, `${id}.json`);
|
|
12898
13560
|
const planPathRelative = `.locus/plans/${id}.json`;
|
|
12899
13561
|
const displayDirective = directive;
|
|
12900
13562
|
process.stderr.write(`
|
|
@@ -12937,7 +13599,7 @@ ${red2("✗")} Planning failed: ${aiResult.error}
|
|
|
12937
13599
|
`);
|
|
12938
13600
|
return;
|
|
12939
13601
|
}
|
|
12940
|
-
if (!
|
|
13602
|
+
if (!existsSync26(planPath)) {
|
|
12941
13603
|
process.stderr.write(`
|
|
12942
13604
|
${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
|
|
12943
13605
|
`);
|
|
@@ -12947,7 +13609,7 @@ ${yellow2("⚠")} Plan file was not created at ${bold2(planPathRelative)}.
|
|
|
12947
13609
|
}
|
|
12948
13610
|
let plan;
|
|
12949
13611
|
try {
|
|
12950
|
-
const content =
|
|
13612
|
+
const content = readFileSync16(planPath, "utf-8");
|
|
12951
13613
|
plan = JSON.parse(content);
|
|
12952
13614
|
} catch {
|
|
12953
13615
|
process.stderr.write(`
|
|
@@ -13109,6 +13771,18 @@ ${bold2("Suggested Order:")}
|
|
|
13109
13771
|
`);
|
|
13110
13772
|
}
|
|
13111
13773
|
}
|
|
13774
|
+
function loadPastMemory(projectRoot) {
|
|
13775
|
+
const memoryDir = getMemoryDir(projectRoot);
|
|
13776
|
+
if (existsSync26(memoryDir)) {
|
|
13777
|
+
const content = readAllMemorySync(projectRoot);
|
|
13778
|
+
if (content.trim()) {
|
|
13779
|
+
return content.length > MEMORY_MAX_CHARS2 ? `${content.slice(0, MEMORY_MAX_CHARS2)}
|
|
13780
|
+
|
|
13781
|
+
...(truncated)` : content;
|
|
13782
|
+
}
|
|
13783
|
+
}
|
|
13784
|
+
return "";
|
|
13785
|
+
}
|
|
13112
13786
|
function buildPlanningPrompt(projectRoot, config, directive, sprintName, id, planPathRelative) {
|
|
13113
13787
|
const parts = [];
|
|
13114
13788
|
parts.push(`<role>
|
|
@@ -13119,18 +13793,17 @@ ${directive}${sprintName ? `
|
|
|
13119
13793
|
|
|
13120
13794
|
**Sprint:** ${sprintName}` : ""}
|
|
13121
13795
|
</directive>`);
|
|
13122
|
-
const locusPath =
|
|
13123
|
-
if (
|
|
13124
|
-
const content =
|
|
13796
|
+
const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
|
|
13797
|
+
if (existsSync26(locusPath)) {
|
|
13798
|
+
const content = readFileSync16(locusPath, "utf-8");
|
|
13125
13799
|
parts.push(`<project-context>
|
|
13126
13800
|
${content.slice(0, 3000)}
|
|
13127
13801
|
</project-context>`);
|
|
13128
13802
|
}
|
|
13129
|
-
const
|
|
13130
|
-
if (
|
|
13131
|
-
const content = readFileSync15(learningsPath, "utf-8");
|
|
13803
|
+
const memoryContent = loadPastMemory(projectRoot);
|
|
13804
|
+
if (memoryContent) {
|
|
13132
13805
|
parts.push(`<past-learnings>
|
|
13133
|
-
${
|
|
13806
|
+
${memoryContent}
|
|
13134
13807
|
</past-learnings>`);
|
|
13135
13808
|
}
|
|
13136
13809
|
parts.push(`<task>
|
|
@@ -13180,9 +13853,9 @@ ${JSON.stringify(plan, null, 2)}
|
|
|
13180
13853
|
parts.push(`<feedback>
|
|
13181
13854
|
${feedback}
|
|
13182
13855
|
</feedback>`);
|
|
13183
|
-
const locusPath =
|
|
13184
|
-
if (
|
|
13185
|
-
const content =
|
|
13856
|
+
const locusPath = join27(projectRoot, ".locus", "LOCUS.md");
|
|
13857
|
+
if (existsSync26(locusPath)) {
|
|
13858
|
+
const content = readFileSync16(locusPath, "utf-8");
|
|
13186
13859
|
parts.push(`<project-context>
|
|
13187
13860
|
${content.slice(0, 3000)}
|
|
13188
13861
|
</project-context>`);
|
|
@@ -13350,10 +14023,12 @@ ${green("✓")} Created ${planned.length} issues.${milestoneTitle ? ` Sprint: ${
|
|
|
13350
14023
|
|
|
13351
14024
|
`);
|
|
13352
14025
|
}
|
|
14026
|
+
var MEMORY_MAX_CHARS2 = 2000;
|
|
13353
14027
|
var init_plan = __esm(() => {
|
|
13354
14028
|
init_run_ai();
|
|
13355
14029
|
init_config();
|
|
13356
14030
|
init_github();
|
|
14031
|
+
init_memory();
|
|
13357
14032
|
init_sandbox();
|
|
13358
14033
|
init_terminal();
|
|
13359
14034
|
});
|
|
@@ -13364,8 +14039,8 @@ __export(exports_review, {
|
|
|
13364
14039
|
reviewCommand: () => reviewCommand
|
|
13365
14040
|
});
|
|
13366
14041
|
import { execFileSync as execFileSync2, execSync as execSync19 } from "node:child_process";
|
|
13367
|
-
import { existsSync as
|
|
13368
|
-
import { join as
|
|
14042
|
+
import { existsSync as existsSync27, readFileSync as readFileSync17 } from "node:fs";
|
|
14043
|
+
import { join as join28 } from "node:path";
|
|
13369
14044
|
function printHelp3() {
|
|
13370
14045
|
process.stderr.write(`
|
|
13371
14046
|
${bold2("locus review")} — AI-powered code review
|
|
@@ -13534,9 +14209,9 @@ function buildReviewPrompt(projectRoot, config, pr, diff, focus) {
|
|
|
13534
14209
|
parts.push(`<role>
|
|
13535
14210
|
You are an expert code reviewer for the ${config.github.owner}/${config.github.repo} repository.
|
|
13536
14211
|
</role>`);
|
|
13537
|
-
const locusPath =
|
|
13538
|
-
if (
|
|
13539
|
-
const content =
|
|
14212
|
+
const locusPath = join28(projectRoot, ".locus", "LOCUS.md");
|
|
14213
|
+
if (existsSync27(locusPath)) {
|
|
14214
|
+
const content = readFileSync17(locusPath, "utf-8");
|
|
13540
14215
|
parts.push(`<project-context>
|
|
13541
14216
|
${content.slice(0, 2000)}
|
|
13542
14217
|
</project-context>`);
|
|
@@ -13851,14 +14526,14 @@ __export(exports_discuss, {
|
|
|
13851
14526
|
discussCommand: () => discussCommand
|
|
13852
14527
|
});
|
|
13853
14528
|
import {
|
|
13854
|
-
existsSync as
|
|
14529
|
+
existsSync as existsSync28,
|
|
13855
14530
|
mkdirSync as mkdirSync18,
|
|
13856
14531
|
readdirSync as readdirSync9,
|
|
13857
|
-
readFileSync as
|
|
14532
|
+
readFileSync as readFileSync18,
|
|
13858
14533
|
unlinkSync as unlinkSync6,
|
|
13859
14534
|
writeFileSync as writeFileSync13
|
|
13860
14535
|
} from "node:fs";
|
|
13861
|
-
import { join as
|
|
14536
|
+
import { join as join29 } from "node:path";
|
|
13862
14537
|
function printHelp5() {
|
|
13863
14538
|
process.stderr.write(`
|
|
13864
14539
|
${bold2("locus discuss")} — AI-powered architectural discussions
|
|
@@ -13880,11 +14555,11 @@ ${bold2("Examples:")}
|
|
|
13880
14555
|
`);
|
|
13881
14556
|
}
|
|
13882
14557
|
function getDiscussionsDir(projectRoot) {
|
|
13883
|
-
return
|
|
14558
|
+
return join29(projectRoot, ".locus", "discussions");
|
|
13884
14559
|
}
|
|
13885
14560
|
function ensureDiscussionsDir(projectRoot) {
|
|
13886
14561
|
const dir = getDiscussionsDir(projectRoot);
|
|
13887
|
-
if (!
|
|
14562
|
+
if (!existsSync28(dir)) {
|
|
13888
14563
|
mkdirSync18(dir, { recursive: true });
|
|
13889
14564
|
}
|
|
13890
14565
|
return dir;
|
|
@@ -13919,7 +14594,7 @@ async function discussCommand(projectRoot, args, flags = {}) {
|
|
|
13919
14594
|
}
|
|
13920
14595
|
function listDiscussions(projectRoot) {
|
|
13921
14596
|
const dir = getDiscussionsDir(projectRoot);
|
|
13922
|
-
if (!
|
|
14597
|
+
if (!existsSync28(dir)) {
|
|
13923
14598
|
process.stderr.write(`${dim2("No discussions yet.")}
|
|
13924
14599
|
`);
|
|
13925
14600
|
return;
|
|
@@ -13936,7 +14611,7 @@ ${bold2("Discussions:")}
|
|
|
13936
14611
|
`);
|
|
13937
14612
|
for (const file of files) {
|
|
13938
14613
|
const id = file.replace(".md", "");
|
|
13939
|
-
const content =
|
|
14614
|
+
const content = readFileSync18(join29(dir, file), "utf-8");
|
|
13940
14615
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
13941
14616
|
const title = titleMatch ? titleMatch[1] : id;
|
|
13942
14617
|
const dateMatch = content.match(/\*\*Date:\*\*\s*(.+)/);
|
|
@@ -13954,7 +14629,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
13954
14629
|
return;
|
|
13955
14630
|
}
|
|
13956
14631
|
const dir = getDiscussionsDir(projectRoot);
|
|
13957
|
-
if (!
|
|
14632
|
+
if (!existsSync28(dir)) {
|
|
13958
14633
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
13959
14634
|
`);
|
|
13960
14635
|
return;
|
|
@@ -13966,7 +14641,7 @@ function showDiscussion(projectRoot, id) {
|
|
|
13966
14641
|
`);
|
|
13967
14642
|
return;
|
|
13968
14643
|
}
|
|
13969
|
-
const content =
|
|
14644
|
+
const content = readFileSync18(join29(dir, match), "utf-8");
|
|
13970
14645
|
process.stdout.write(`${content}
|
|
13971
14646
|
`);
|
|
13972
14647
|
}
|
|
@@ -13977,7 +14652,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
13977
14652
|
return;
|
|
13978
14653
|
}
|
|
13979
14654
|
const dir = getDiscussionsDir(projectRoot);
|
|
13980
|
-
if (!
|
|
14655
|
+
if (!existsSync28(dir)) {
|
|
13981
14656
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
13982
14657
|
`);
|
|
13983
14658
|
return;
|
|
@@ -13989,7 +14664,7 @@ function deleteDiscussion(projectRoot, id) {
|
|
|
13989
14664
|
`);
|
|
13990
14665
|
return;
|
|
13991
14666
|
}
|
|
13992
|
-
unlinkSync6(
|
|
14667
|
+
unlinkSync6(join29(dir, match));
|
|
13993
14668
|
process.stderr.write(`${green("✓")} Deleted discussion: ${match.replace(".md", "")}
|
|
13994
14669
|
`);
|
|
13995
14670
|
}
|
|
@@ -14002,7 +14677,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
14002
14677
|
return;
|
|
14003
14678
|
}
|
|
14004
14679
|
const dir = getDiscussionsDir(projectRoot);
|
|
14005
|
-
if (!
|
|
14680
|
+
if (!existsSync28(dir)) {
|
|
14006
14681
|
process.stderr.write(`${red2("✗")} No discussions found.
|
|
14007
14682
|
`);
|
|
14008
14683
|
return;
|
|
@@ -14014,7 +14689,7 @@ async function convertDiscussionToPlan(projectRoot, id) {
|
|
|
14014
14689
|
`);
|
|
14015
14690
|
return;
|
|
14016
14691
|
}
|
|
14017
|
-
const content =
|
|
14692
|
+
const content = readFileSync18(join29(dir, match), "utf-8");
|
|
14018
14693
|
const titleMatch = content.match(/^#\s+(.+)/m);
|
|
14019
14694
|
const discussionTitle = titleMatch ? titleMatch[1].trim() : id;
|
|
14020
14695
|
await planCommand(projectRoot, [
|
|
@@ -14140,7 +14815,7 @@ ${turn.content}`;
|
|
|
14140
14815
|
...conversation.length > 1 ? [`---`, ``, `## Discussion Transcript`, ``, transcript, ``] : []
|
|
14141
14816
|
].join(`
|
|
14142
14817
|
`);
|
|
14143
|
-
writeFileSync13(
|
|
14818
|
+
writeFileSync13(join29(dir, `${id}.md`), markdown, "utf-8");
|
|
14144
14819
|
process.stderr.write(`
|
|
14145
14820
|
${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
|
|
14146
14821
|
`);
|
|
@@ -14150,23 +14825,34 @@ ${green("✓")} Discussion saved: ${cyan2(id)} ${dim2(`(${timer.formatted()})`)}
|
|
|
14150
14825
|
|
|
14151
14826
|
`);
|
|
14152
14827
|
}
|
|
14828
|
+
function loadPastMemory2(projectRoot) {
|
|
14829
|
+
const memoryDir = getMemoryDir(projectRoot);
|
|
14830
|
+
if (existsSync28(memoryDir)) {
|
|
14831
|
+
const content = readAllMemorySync(projectRoot);
|
|
14832
|
+
if (content.trim()) {
|
|
14833
|
+
return content.length > MEMORY_MAX_CHARS3 ? `${content.slice(0, MEMORY_MAX_CHARS3)}
|
|
14834
|
+
|
|
14835
|
+
...(truncated)` : content;
|
|
14836
|
+
}
|
|
14837
|
+
}
|
|
14838
|
+
return "";
|
|
14839
|
+
}
|
|
14153
14840
|
function buildDiscussionPrompt(projectRoot, config, topic, conversation, forceFinal) {
|
|
14154
14841
|
const parts = [];
|
|
14155
14842
|
parts.push(`<role>
|
|
14156
14843
|
You are a senior software architect and consultant for the ${config.github.owner}/${config.github.repo} project.
|
|
14157
14844
|
</role>`);
|
|
14158
|
-
const locusPath =
|
|
14159
|
-
if (
|
|
14160
|
-
const content =
|
|
14845
|
+
const locusPath = join29(projectRoot, ".locus", "LOCUS.md");
|
|
14846
|
+
if (existsSync28(locusPath)) {
|
|
14847
|
+
const content = readFileSync18(locusPath, "utf-8");
|
|
14161
14848
|
parts.push(`<project-context>
|
|
14162
14849
|
${content.slice(0, 3000)}
|
|
14163
14850
|
</project-context>`);
|
|
14164
14851
|
}
|
|
14165
|
-
const
|
|
14166
|
-
if (
|
|
14167
|
-
const content = readFileSync17(learningsPath, "utf-8");
|
|
14852
|
+
const memoryContent = loadPastMemory2(projectRoot);
|
|
14853
|
+
if (memoryContent) {
|
|
14168
14854
|
parts.push(`<past-learnings>
|
|
14169
|
-
${
|
|
14855
|
+
${memoryContent}
|
|
14170
14856
|
</past-learnings>`);
|
|
14171
14857
|
}
|
|
14172
14858
|
parts.push(`<discussion-topic>
|
|
@@ -14215,10 +14901,11 @@ If you still need key information to give a good answer:
|
|
|
14215
14901
|
|
|
14216
14902
|
`);
|
|
14217
14903
|
}
|
|
14218
|
-
var MAX_DISCUSSION_ROUNDS = 5;
|
|
14904
|
+
var MAX_DISCUSSION_ROUNDS = 5, MEMORY_MAX_CHARS3 = 2000;
|
|
14219
14905
|
var init_discuss = __esm(() => {
|
|
14220
14906
|
init_run_ai();
|
|
14221
14907
|
init_config();
|
|
14908
|
+
init_memory();
|
|
14222
14909
|
init_sandbox();
|
|
14223
14910
|
init_progress();
|
|
14224
14911
|
init_terminal();
|
|
@@ -14235,8 +14922,8 @@ __export(exports_artifacts, {
|
|
|
14235
14922
|
formatDate: () => formatDate2,
|
|
14236
14923
|
artifactsCommand: () => artifactsCommand
|
|
14237
14924
|
});
|
|
14238
|
-
import { existsSync as
|
|
14239
|
-
import { join as
|
|
14925
|
+
import { existsSync as existsSync29, readdirSync as readdirSync10, readFileSync as readFileSync19, statSync as statSync5 } from "node:fs";
|
|
14926
|
+
import { join as join30 } from "node:path";
|
|
14240
14927
|
function printHelp6() {
|
|
14241
14928
|
process.stderr.write(`
|
|
14242
14929
|
${bold2("locus artifacts")} — View and manage AI-generated artifacts
|
|
@@ -14256,37 +14943,37 @@ ${dim2("Artifact names support partial matching.")}
|
|
|
14256
14943
|
`);
|
|
14257
14944
|
}
|
|
14258
14945
|
function getArtifactsDir(projectRoot) {
|
|
14259
|
-
return
|
|
14946
|
+
return join30(projectRoot, ".locus", "artifacts");
|
|
14260
14947
|
}
|
|
14261
14948
|
function listArtifacts(projectRoot) {
|
|
14262
14949
|
const dir = getArtifactsDir(projectRoot);
|
|
14263
|
-
if (!
|
|
14950
|
+
if (!existsSync29(dir))
|
|
14264
14951
|
return [];
|
|
14265
14952
|
return readdirSync10(dir).filter((f) => f.endsWith(".md")).map((fileName) => {
|
|
14266
|
-
const filePath =
|
|
14267
|
-
const
|
|
14953
|
+
const filePath = join30(dir, fileName);
|
|
14954
|
+
const stat2 = statSync5(filePath);
|
|
14268
14955
|
return {
|
|
14269
14956
|
name: fileName.replace(/\.md$/, ""),
|
|
14270
14957
|
fileName,
|
|
14271
|
-
createdAt:
|
|
14272
|
-
size:
|
|
14958
|
+
createdAt: stat2.birthtime,
|
|
14959
|
+
size: stat2.size
|
|
14273
14960
|
};
|
|
14274
14961
|
}).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
14275
14962
|
}
|
|
14276
14963
|
function readArtifact(projectRoot, name) {
|
|
14277
14964
|
const dir = getArtifactsDir(projectRoot);
|
|
14278
14965
|
const fileName = name.endsWith(".md") ? name : `${name}.md`;
|
|
14279
|
-
const filePath =
|
|
14280
|
-
if (!
|
|
14966
|
+
const filePath = join30(dir, fileName);
|
|
14967
|
+
if (!existsSync29(filePath))
|
|
14281
14968
|
return null;
|
|
14282
|
-
const
|
|
14969
|
+
const stat2 = statSync5(filePath);
|
|
14283
14970
|
return {
|
|
14284
|
-
content:
|
|
14971
|
+
content: readFileSync19(filePath, "utf-8"),
|
|
14285
14972
|
info: {
|
|
14286
14973
|
name: fileName.replace(/\.md$/, ""),
|
|
14287
14974
|
fileName,
|
|
14288
|
-
createdAt:
|
|
14289
|
-
size:
|
|
14975
|
+
createdAt: stat2.birthtime,
|
|
14976
|
+
size: stat2.size
|
|
14290
14977
|
}
|
|
14291
14978
|
};
|
|
14292
14979
|
}
|
|
@@ -14581,7 +15268,7 @@ Co-Authored-By: LocusAgent <agent@locusai.team>`;
|
|
|
14581
15268
|
`);
|
|
14582
15269
|
}
|
|
14583
15270
|
}
|
|
14584
|
-
function buildCommitPrompt(diff,
|
|
15271
|
+
function buildCommitPrompt(diff, stat2, recentCommits) {
|
|
14585
15272
|
const maxDiffLength = 15000;
|
|
14586
15273
|
const truncatedDiff = diff.length > maxDiffLength ? `${diff.slice(0, maxDiffLength)}
|
|
14587
15274
|
|
|
@@ -14604,7 +15291,7 @@ ${recentCommits}
|
|
|
14604
15291
|
`;
|
|
14605
15292
|
}
|
|
14606
15293
|
prompt += `File summary:
|
|
14607
|
-
${
|
|
15294
|
+
${stat2}
|
|
14608
15295
|
|
|
14609
15296
|
Diff:
|
|
14610
15297
|
${truncatedDiff}`;
|
|
@@ -14638,10 +15325,10 @@ __export(exports_sandbox2, {
|
|
|
14638
15325
|
parseSandboxLogsArgs: () => parseSandboxLogsArgs,
|
|
14639
15326
|
parseSandboxInstallArgs: () => parseSandboxInstallArgs
|
|
14640
15327
|
});
|
|
14641
|
-
import { execSync as execSync22, spawn as
|
|
15328
|
+
import { execSync as execSync22, spawn as spawn8 } from "node:child_process";
|
|
14642
15329
|
import { createHash as createHash2 } from "node:crypto";
|
|
14643
|
-
import { existsSync as
|
|
14644
|
-
import { basename as basename4, join as
|
|
15330
|
+
import { existsSync as existsSync30, readFileSync as readFileSync20 } from "node:fs";
|
|
15331
|
+
import { basename as basename4, join as join31 } from "node:path";
|
|
14645
15332
|
import { createInterface as createInterface3 } from "node:readline";
|
|
14646
15333
|
function printSandboxHelp() {
|
|
14647
15334
|
process.stderr.write(`
|
|
@@ -14830,7 +15517,7 @@ async function handleAgentLogin(projectRoot, agent) {
|
|
|
14830
15517
|
process.stderr.write(`${dim2("Login and then exit when ready.")}
|
|
14831
15518
|
|
|
14832
15519
|
`);
|
|
14833
|
-
const child =
|
|
15520
|
+
const child = spawn8("docker", [
|
|
14834
15521
|
"sandbox",
|
|
14835
15522
|
"exec",
|
|
14836
15523
|
"--privileged",
|
|
@@ -15171,7 +15858,7 @@ async function handleLogs(projectRoot, args) {
|
|
|
15171
15858
|
}
|
|
15172
15859
|
function detectPackageManager2(projectRoot) {
|
|
15173
15860
|
try {
|
|
15174
|
-
const raw =
|
|
15861
|
+
const raw = readFileSync20(join31(projectRoot, "package.json"), "utf-8");
|
|
15175
15862
|
const pkgJson = JSON.parse(raw);
|
|
15176
15863
|
if (typeof pkgJson.packageManager === "string") {
|
|
15177
15864
|
const name = pkgJson.packageManager.split("@")[0];
|
|
@@ -15180,13 +15867,13 @@ function detectPackageManager2(projectRoot) {
|
|
|
15180
15867
|
}
|
|
15181
15868
|
}
|
|
15182
15869
|
} catch {}
|
|
15183
|
-
if (
|
|
15870
|
+
if (existsSync30(join31(projectRoot, "bun.lock")) || existsSync30(join31(projectRoot, "bun.lockb"))) {
|
|
15184
15871
|
return "bun";
|
|
15185
15872
|
}
|
|
15186
|
-
if (
|
|
15873
|
+
if (existsSync30(join31(projectRoot, "yarn.lock"))) {
|
|
15187
15874
|
return "yarn";
|
|
15188
15875
|
}
|
|
15189
|
-
if (
|
|
15876
|
+
if (existsSync30(join31(projectRoot, "pnpm-lock.yaml"))) {
|
|
15190
15877
|
return "pnpm";
|
|
15191
15878
|
}
|
|
15192
15879
|
return "npm";
|
|
@@ -15289,9 +15976,9 @@ Installing sandbox dependencies (${bold2(installCmd.join(" "))}) to container fi
|
|
|
15289
15976
|
${dim2(`Detected ${ecosystem} project — skipping JS package install.`)}
|
|
15290
15977
|
`);
|
|
15291
15978
|
}
|
|
15292
|
-
const setupScript =
|
|
15293
|
-
const containerSetupScript = containerWorkdir ?
|
|
15294
|
-
if (
|
|
15979
|
+
const setupScript = join31(projectRoot, ".locus", "sandbox-setup.sh");
|
|
15980
|
+
const containerSetupScript = containerWorkdir ? join31(containerWorkdir, ".locus", "sandbox-setup.sh") : setupScript;
|
|
15981
|
+
if (existsSync30(setupScript)) {
|
|
15295
15982
|
process.stderr.write(`Running ${bold2(".locus/sandbox-setup.sh")} in sandbox ${dim2(sandboxName)}...
|
|
15296
15983
|
`);
|
|
15297
15984
|
const hookOk = await runInteractiveCommand("docker", [
|
|
@@ -15373,7 +16060,7 @@ function getActiveProviderSandbox(projectRoot, provider) {
|
|
|
15373
16060
|
}
|
|
15374
16061
|
function runInteractiveCommand(command, args) {
|
|
15375
16062
|
return new Promise((resolve2) => {
|
|
15376
|
-
const child =
|
|
16063
|
+
const child = spawn8(command, args, { stdio: "inherit" });
|
|
15377
16064
|
child.on("close", (code) => resolve2(code === 0));
|
|
15378
16065
|
child.on("error", () => resolve2(false));
|
|
15379
16066
|
});
|
|
@@ -15458,23 +16145,295 @@ var init_sandbox2 = __esm(() => {
|
|
|
15458
16145
|
PROVIDERS = ["claude", "codex"];
|
|
15459
16146
|
});
|
|
15460
16147
|
|
|
16148
|
+
// src/commands/memory.ts
|
|
16149
|
+
var exports_memory = {};
|
|
16150
|
+
__export(exports_memory, {
|
|
16151
|
+
memoryCommand: () => memoryCommand
|
|
16152
|
+
});
|
|
16153
|
+
import { existsSync as existsSync31 } from "node:fs";
|
|
16154
|
+
import { writeFile as writeFile2 } from "node:fs/promises";
|
|
16155
|
+
import { join as join32 } from "node:path";
|
|
16156
|
+
import { createInterface as createInterface4 } from "node:readline";
|
|
16157
|
+
function printHelp7() {
|
|
16158
|
+
process.stderr.write(`
|
|
16159
|
+
${bold2("locus memory")} — Inspect, search, and manage structured memory
|
|
16160
|
+
|
|
16161
|
+
${bold2("Usage:")}
|
|
16162
|
+
locus memory list [--category <name>] List all memory entries
|
|
16163
|
+
locus memory search <query> Search entries by keyword
|
|
16164
|
+
locus memory stats Show per-category statistics
|
|
16165
|
+
locus memory reset [--confirm] Clear all entries (preserve headers)
|
|
16166
|
+
locus memory migrate Migrate legacy LEARNINGS.md → memory/
|
|
16167
|
+
|
|
16168
|
+
${bold2("Categories:")}
|
|
16169
|
+
architecture, conventions, decisions, preferences, debugging
|
|
16170
|
+
|
|
16171
|
+
${bold2("Examples:")}
|
|
16172
|
+
locus memory list ${dim2("# Show all entries")}
|
|
16173
|
+
locus memory list --category debugging ${dim2("# Show only debugging entries")}
|
|
16174
|
+
locus memory search "sandbox" ${dim2("# Search for 'sandbox'")}
|
|
16175
|
+
locus memory stats ${dim2("# Show entry counts and sizes")}
|
|
16176
|
+
locus memory reset --confirm ${dim2("# Clear all entries")}
|
|
16177
|
+
locus memory migrate ${dim2("# Migrate legacy LEARNINGS.md")}
|
|
16178
|
+
`);
|
|
16179
|
+
}
|
|
16180
|
+
async function memoryCommand(projectRoot, args) {
|
|
16181
|
+
const subcommand = args[0];
|
|
16182
|
+
if (!subcommand || subcommand === "help") {
|
|
16183
|
+
printHelp7();
|
|
16184
|
+
return;
|
|
16185
|
+
}
|
|
16186
|
+
const memoryDir = getMemoryDir(projectRoot);
|
|
16187
|
+
if (!existsSync31(memoryDir) && subcommand !== "migrate") {
|
|
16188
|
+
process.stderr.write(`${red2("✗")} Memory directory not found at ${dim2(".locus/memory/")}
|
|
16189
|
+
`);
|
|
16190
|
+
process.stderr.write(` Run ${bold2("locus init")} to create the memory directory.
|
|
16191
|
+
`);
|
|
16192
|
+
process.exit(1);
|
|
16193
|
+
}
|
|
16194
|
+
switch (subcommand) {
|
|
16195
|
+
case "list":
|
|
16196
|
+
await handleList(projectRoot, args.slice(1));
|
|
16197
|
+
break;
|
|
16198
|
+
case "search":
|
|
16199
|
+
await handleSearch(projectRoot, args.slice(1));
|
|
16200
|
+
break;
|
|
16201
|
+
case "stats":
|
|
16202
|
+
await handleStats(projectRoot);
|
|
16203
|
+
break;
|
|
16204
|
+
case "reset":
|
|
16205
|
+
await handleReset(projectRoot, args.slice(1));
|
|
16206
|
+
break;
|
|
16207
|
+
case "migrate":
|
|
16208
|
+
await handleMigrate(projectRoot);
|
|
16209
|
+
break;
|
|
16210
|
+
default:
|
|
16211
|
+
process.stderr.write(`${red2("✗")} Unknown subcommand: ${bold2(subcommand)}
|
|
16212
|
+
`);
|
|
16213
|
+
process.stderr.write(` Run ${bold2("locus memory help")} for available subcommands.
|
|
16214
|
+
`);
|
|
16215
|
+
process.exit(1);
|
|
16216
|
+
}
|
|
16217
|
+
}
|
|
16218
|
+
async function handleList(projectRoot, args) {
|
|
16219
|
+
let categoryFilter;
|
|
16220
|
+
const catIdx = args.indexOf("--category");
|
|
16221
|
+
if (catIdx !== -1) {
|
|
16222
|
+
categoryFilter = args[catIdx + 1];
|
|
16223
|
+
if (!categoryFilter || !MEMORY_CATEGORIES[categoryFilter]) {
|
|
16224
|
+
const valid = Object.keys(MEMORY_CATEGORIES).join(", ");
|
|
16225
|
+
process.stderr.write(`${red2("✗")} Invalid category. Valid categories: ${valid}
|
|
16226
|
+
`);
|
|
16227
|
+
process.exit(1);
|
|
16228
|
+
}
|
|
16229
|
+
}
|
|
16230
|
+
const categories = categoryFilter ? [categoryFilter] : Object.keys(MEMORY_CATEGORIES);
|
|
16231
|
+
let totalEntries = 0;
|
|
16232
|
+
for (const category of categories) {
|
|
16233
|
+
const content = await readMemoryFile(projectRoot, category);
|
|
16234
|
+
const entries = parseEntries(content);
|
|
16235
|
+
if (entries.length === 0)
|
|
16236
|
+
continue;
|
|
16237
|
+
const meta = MEMORY_CATEGORIES[category];
|
|
16238
|
+
process.stderr.write(`
|
|
16239
|
+
${bold2(cyan2(meta.title))} ${dim2(`(${meta.description})`)}
|
|
16240
|
+
`);
|
|
16241
|
+
for (const entry of entries) {
|
|
16242
|
+
process.stderr.write(` ${entry}
|
|
16243
|
+
`);
|
|
16244
|
+
}
|
|
16245
|
+
totalEntries += entries.length;
|
|
16246
|
+
}
|
|
16247
|
+
if (totalEntries === 0) {
|
|
16248
|
+
process.stderr.write(`
|
|
16249
|
+
${dim2("No memory entries found.")}
|
|
16250
|
+
`);
|
|
16251
|
+
} else {
|
|
16252
|
+
process.stderr.write(`
|
|
16253
|
+
${dim2(`${totalEntries} entries total`)}
|
|
16254
|
+
`);
|
|
16255
|
+
}
|
|
16256
|
+
}
|
|
16257
|
+
async function handleSearch(projectRoot, args) {
|
|
16258
|
+
const query = args.join(" ").trim();
|
|
16259
|
+
if (!query) {
|
|
16260
|
+
process.stderr.write(`${red2("✗")} Usage: locus memory search <query>
|
|
16261
|
+
`);
|
|
16262
|
+
process.exit(1);
|
|
16263
|
+
}
|
|
16264
|
+
const queryLower = query.toLowerCase();
|
|
16265
|
+
let matchCount = 0;
|
|
16266
|
+
for (const [category, meta] of Object.entries(MEMORY_CATEGORIES)) {
|
|
16267
|
+
const content = await readMemoryFile(projectRoot, category);
|
|
16268
|
+
const entries = parseEntries(content);
|
|
16269
|
+
const matches = entries.filter((entry) => entry.toLowerCase().includes(queryLower));
|
|
16270
|
+
if (matches.length === 0)
|
|
16271
|
+
continue;
|
|
16272
|
+
process.stderr.write(`
|
|
16273
|
+
${bold2(cyan2(meta.title))}
|
|
16274
|
+
`);
|
|
16275
|
+
for (const entry of matches) {
|
|
16276
|
+
const highlighted = highlightMatch(entry, query);
|
|
16277
|
+
process.stderr.write(` ${highlighted}
|
|
16278
|
+
`);
|
|
16279
|
+
}
|
|
16280
|
+
matchCount += matches.length;
|
|
16281
|
+
}
|
|
16282
|
+
if (matchCount === 0) {
|
|
16283
|
+
process.stderr.write(`
|
|
16284
|
+
${dim2("No matches found for")} "${query}"
|
|
16285
|
+
`);
|
|
16286
|
+
} else {
|
|
16287
|
+
process.stderr.write(`
|
|
16288
|
+
${green(`${matchCount} match${matchCount === 1 ? "" : "es"}`)} found for "${query}"
|
|
16289
|
+
`);
|
|
16290
|
+
}
|
|
16291
|
+
}
|
|
16292
|
+
async function handleStats(projectRoot) {
|
|
16293
|
+
const stats = await getMemoryStats(projectRoot);
|
|
16294
|
+
process.stderr.write(`
|
|
16295
|
+
${bold2("Memory Statistics")}
|
|
16296
|
+
|
|
16297
|
+
`);
|
|
16298
|
+
let totalEntries = 0;
|
|
16299
|
+
let totalSize = 0;
|
|
16300
|
+
for (const [key, meta] of Object.entries(MEMORY_CATEGORIES)) {
|
|
16301
|
+
const s = stats[key];
|
|
16302
|
+
const countStr = String(s.count).padStart(3);
|
|
16303
|
+
const sizeStr = formatSize2(s.size).padStart(8);
|
|
16304
|
+
const dateStr = s.size > 0 ? dim2(s.lastModified.toLocaleDateString("en-US", {
|
|
16305
|
+
month: "short",
|
|
16306
|
+
day: "numeric",
|
|
16307
|
+
year: "numeric"
|
|
16308
|
+
})) : dim2("—");
|
|
16309
|
+
const icon = s.count > 0 ? green("●") : dim2("○");
|
|
16310
|
+
process.stderr.write(` ${icon} ${cyan2(meta.title.padEnd(14))} ${countStr} entries ${sizeStr} ${dateStr}
|
|
16311
|
+
`);
|
|
16312
|
+
totalEntries += s.count;
|
|
16313
|
+
totalSize += s.size;
|
|
16314
|
+
}
|
|
16315
|
+
process.stderr.write(`
|
|
16316
|
+
${bold2("Total:")} ${totalEntries} entries, ${formatSize2(totalSize)}
|
|
16317
|
+
`);
|
|
16318
|
+
process.stderr.write(`
|
|
16319
|
+
`);
|
|
16320
|
+
}
|
|
16321
|
+
async function handleReset(projectRoot, args) {
|
|
16322
|
+
const confirmed = args.includes("--confirm");
|
|
16323
|
+
if (!confirmed) {
|
|
16324
|
+
const rl = createInterface4({
|
|
16325
|
+
input: process.stdin,
|
|
16326
|
+
output: process.stderr
|
|
16327
|
+
});
|
|
16328
|
+
const answer = await new Promise((resolve2) => {
|
|
16329
|
+
rl.question(`${yellow2("⚠")} This will clear all memory entries. Continue? (y/N) `, (ans) => {
|
|
16330
|
+
rl.close();
|
|
16331
|
+
resolve2(ans.trim().toLowerCase());
|
|
16332
|
+
});
|
|
16333
|
+
});
|
|
16334
|
+
if (answer !== "y" && answer !== "yes") {
|
|
16335
|
+
process.stderr.write(`${dim2("Cancelled.")}
|
|
16336
|
+
`);
|
|
16337
|
+
return;
|
|
16338
|
+
}
|
|
16339
|
+
}
|
|
16340
|
+
const stats = await getMemoryStats(projectRoot);
|
|
16341
|
+
const memoryDir = getMemoryDir(projectRoot);
|
|
16342
|
+
let totalCleared = 0;
|
|
16343
|
+
for (const [key, meta] of Object.entries(MEMORY_CATEGORIES)) {
|
|
16344
|
+
const count = stats[key].count;
|
|
16345
|
+
const filePath = join32(memoryDir, meta.file);
|
|
16346
|
+
const header = `# ${meta.title}
|
|
16347
|
+
|
|
16348
|
+
${meta.description}
|
|
16349
|
+
|
|
16350
|
+
`;
|
|
16351
|
+
await writeFile2(filePath, header, "utf-8");
|
|
16352
|
+
if (count > 0) {
|
|
16353
|
+
process.stderr.write(` ${dim2("○")} ${meta.title}: cleared ${count} entries
|
|
16354
|
+
`);
|
|
16355
|
+
totalCleared += count;
|
|
16356
|
+
}
|
|
16357
|
+
}
|
|
16358
|
+
if (totalCleared > 0) {
|
|
16359
|
+
process.stderr.write(`
|
|
16360
|
+
${green("✓")} Cleared ${totalCleared} entries (file headers preserved)
|
|
16361
|
+
`);
|
|
16362
|
+
} else {
|
|
16363
|
+
process.stderr.write(`${dim2("No entries to clear.")}
|
|
16364
|
+
`);
|
|
16365
|
+
}
|
|
16366
|
+
}
|
|
16367
|
+
async function handleMigrate(projectRoot) {
|
|
16368
|
+
const learningsPath = join32(projectRoot, ".locus", "LEARNINGS.md");
|
|
16369
|
+
if (!existsSync31(learningsPath)) {
|
|
16370
|
+
process.stderr.write(`${dim2("○")} No LEARNINGS.md found — nothing to migrate.
|
|
16371
|
+
`);
|
|
16372
|
+
return;
|
|
16373
|
+
}
|
|
16374
|
+
process.stderr.write(`Migrating entries from LEARNINGS.md...
|
|
16375
|
+
`);
|
|
16376
|
+
const result = await migrateFromLearnings(projectRoot);
|
|
16377
|
+
if (result.migrated > 0) {
|
|
16378
|
+
process.stderr.write(`${green("✓")} Migrated ${result.migrated} entries to .locus/memory/
|
|
16379
|
+
`);
|
|
16380
|
+
}
|
|
16381
|
+
if (result.skipped > 0) {
|
|
16382
|
+
process.stderr.write(`${dim2("○")} Skipped ${result.skipped} duplicate entries
|
|
16383
|
+
`);
|
|
16384
|
+
}
|
|
16385
|
+
if (result.migrated > 0 || result.skipped > 0) {
|
|
16386
|
+
process.stderr.write(`${green("✓")} Removed legacy LEARNINGS.md
|
|
16387
|
+
`);
|
|
16388
|
+
}
|
|
16389
|
+
if (result.migrated === 0 && result.skipped === 0) {
|
|
16390
|
+
process.stderr.write(`${dim2("○")} No entries found in LEARNINGS.md to migrate.
|
|
16391
|
+
`);
|
|
16392
|
+
}
|
|
16393
|
+
}
|
|
16394
|
+
function parseEntries(content) {
|
|
16395
|
+
if (!content)
|
|
16396
|
+
return [];
|
|
16397
|
+
return content.split(`
|
|
16398
|
+
`).filter((line) => line.startsWith("- "));
|
|
16399
|
+
}
|
|
16400
|
+
function highlightMatch(text, query) {
|
|
16401
|
+
const regex = new RegExp(`(${escapeRegex(query)})`, "gi");
|
|
16402
|
+
return text.replace(regex, (match) => bold2(match));
|
|
16403
|
+
}
|
|
16404
|
+
function escapeRegex(str) {
|
|
16405
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16406
|
+
}
|
|
16407
|
+
function formatSize2(bytes) {
|
|
16408
|
+
if (bytes === 0)
|
|
16409
|
+
return "0 B";
|
|
16410
|
+
if (bytes < 1024)
|
|
16411
|
+
return `${bytes} B`;
|
|
16412
|
+
const kb = bytes / 1024;
|
|
16413
|
+
return `${kb.toFixed(1)} KB`;
|
|
16414
|
+
}
|
|
16415
|
+
var init_memory2 = __esm(() => {
|
|
16416
|
+
init_memory();
|
|
16417
|
+
init_terminal();
|
|
16418
|
+
});
|
|
16419
|
+
|
|
15461
16420
|
// src/cli.ts
|
|
15462
16421
|
init_config();
|
|
15463
16422
|
init_context();
|
|
15464
16423
|
init_logger();
|
|
15465
16424
|
init_rate_limiter();
|
|
15466
16425
|
init_terminal();
|
|
15467
|
-
import { existsSync as
|
|
15468
|
-
import { join as
|
|
16426
|
+
import { existsSync as existsSync32, readFileSync as readFileSync21 } from "node:fs";
|
|
16427
|
+
import { join as join33 } from "node:path";
|
|
15469
16428
|
import { fileURLToPath } from "node:url";
|
|
15470
16429
|
function getCliVersion() {
|
|
15471
16430
|
const fallbackVersion = "0.0.0";
|
|
15472
|
-
const packageJsonPath =
|
|
15473
|
-
if (!
|
|
16431
|
+
const packageJsonPath = join33(fileURLToPath(new URL(".", import.meta.url)), "..", "package.json");
|
|
16432
|
+
if (!existsSync32(packageJsonPath)) {
|
|
15474
16433
|
return fallbackVersion;
|
|
15475
16434
|
}
|
|
15476
16435
|
try {
|
|
15477
|
-
const parsed = JSON.parse(
|
|
16436
|
+
const parsed = JSON.parse(readFileSync21(packageJsonPath, "utf-8"));
|
|
15478
16437
|
return parsed.version ?? fallbackVersion;
|
|
15479
16438
|
} catch {
|
|
15480
16439
|
return fallbackVersion;
|
|
@@ -15636,7 +16595,7 @@ function printLogo() {
|
|
|
15636
16595
|
`);
|
|
15637
16596
|
}
|
|
15638
16597
|
}
|
|
15639
|
-
function
|
|
16598
|
+
function printHelp8() {
|
|
15640
16599
|
printLogo();
|
|
15641
16600
|
process.stderr.write(`
|
|
15642
16601
|
|
|
@@ -15664,6 +16623,7 @@ ${bold2("Commands:")}
|
|
|
15664
16623
|
${cyan2("packages")} Manage installed packages (list, outdated)
|
|
15665
16624
|
${cyan2("pkg")} ${dim2("<name> [cmd]")} Run a command from an installed package
|
|
15666
16625
|
${cyan2("skills")} Discover and manage agent skills
|
|
16626
|
+
${cyan2("memory")} Inspect, search, and manage memory
|
|
15667
16627
|
${cyan2("sandbox")} Manage Docker sandbox lifecycle
|
|
15668
16628
|
${cyan2("upgrade")} Check for and install updates
|
|
15669
16629
|
|
|
@@ -15746,7 +16706,7 @@ async function main() {
|
|
|
15746
16706
|
process.exit(0);
|
|
15747
16707
|
}
|
|
15748
16708
|
if (parsed.flags.help && !parsed.command) {
|
|
15749
|
-
|
|
16709
|
+
printHelp8();
|
|
15750
16710
|
process.exit(0);
|
|
15751
16711
|
}
|
|
15752
16712
|
const command = resolveAlias(parsed.command);
|
|
@@ -15757,7 +16717,7 @@ async function main() {
|
|
|
15757
16717
|
try {
|
|
15758
16718
|
const root = getGitRoot(cwd);
|
|
15759
16719
|
if (isInitialized(root)) {
|
|
15760
|
-
logDir =
|
|
16720
|
+
logDir = join33(root, ".locus", "logs");
|
|
15761
16721
|
getRateLimiter(root);
|
|
15762
16722
|
}
|
|
15763
16723
|
} catch {}
|
|
@@ -15778,7 +16738,7 @@ async function main() {
|
|
|
15778
16738
|
printVersionNotice = startVersionCheck2(VERSION);
|
|
15779
16739
|
}
|
|
15780
16740
|
if (!command) {
|
|
15781
|
-
|
|
16741
|
+
printHelp8();
|
|
15782
16742
|
process.exit(0);
|
|
15783
16743
|
}
|
|
15784
16744
|
if (command === "init") {
|
|
@@ -15974,6 +16934,12 @@ async function main() {
|
|
|
15974
16934
|
await sandboxCommand2(projectRoot, sandboxArgs);
|
|
15975
16935
|
break;
|
|
15976
16936
|
}
|
|
16937
|
+
case "memory": {
|
|
16938
|
+
const { memoryCommand: memoryCommand2 } = await Promise.resolve().then(() => (init_memory2(), exports_memory));
|
|
16939
|
+
const memoryArgs = parsed.flags.help ? ["help"] : parsed.args;
|
|
16940
|
+
await memoryCommand2(projectRoot, memoryArgs);
|
|
16941
|
+
break;
|
|
16942
|
+
}
|
|
15977
16943
|
case "upgrade": {
|
|
15978
16944
|
const { upgradeCommand: upgradeCommand2 } = await Promise.resolve().then(() => (init_upgrade(), exports_upgrade));
|
|
15979
16945
|
await upgradeCommand2(projectRoot, parsed.args, {
|