reasonix 0.4.19 → 0.4.21
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/README.md +47 -2
- package/dist/cli/chunk-K6MR4SWS.js +601 -0
- package/dist/cli/chunk-K6MR4SWS.js.map +1 -0
- package/dist/cli/index.js +460 -55
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{prompt-JNNNJLYF.js → prompt-VDN5U3YE.js} +2 -2
- package/dist/index.d.ts +149 -5
- package/dist/index.js +658 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/chunk-HNEWBEWZ.js +0 -152
- package/dist/cli/chunk-HNEWBEWZ.js.map +0 -1
- /package/dist/cli/{prompt-JNNNJLYF.js.map → prompt-VDN5U3YE.js.map} +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
MemoryStore,
|
|
3
4
|
PROJECT_MEMORY_FILE,
|
|
4
|
-
|
|
5
|
+
SkillStore,
|
|
6
|
+
applyMemoryStack,
|
|
5
7
|
memoryEnabled,
|
|
6
|
-
readProjectMemory
|
|
7
|
-
|
|
8
|
+
readProjectMemory,
|
|
9
|
+
sanitizeMemoryName
|
|
10
|
+
} from "./chunk-K6MR4SWS.js";
|
|
8
11
|
|
|
9
12
|
// src/cli/index.ts
|
|
10
13
|
import { Command } from "commander";
|
|
@@ -2206,7 +2209,7 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2206
2209
|
});
|
|
2207
2210
|
registry.register({
|
|
2208
2211
|
name: "edit_file",
|
|
2209
|
-
description: "Apply a SEARCH/REPLACE edit to an existing file. `search` must match exactly (whitespace sensitive) \u2014 no regex. The match must be unique in the file; otherwise the edit is refused to avoid surprise rewrites.
|
|
2212
|
+
description: "Apply a SEARCH/REPLACE edit to an existing file. `search` must match exactly (whitespace sensitive) \u2014 no regex. The match must be unique in the file; otherwise the edit is refused to avoid surprise rewrites.",
|
|
2210
2213
|
parameters: {
|
|
2211
2214
|
type: "object",
|
|
2212
2215
|
properties: {
|
|
@@ -2323,6 +2326,127 @@ function lineDiff(a, b) {
|
|
|
2323
2326
|
return out;
|
|
2324
2327
|
}
|
|
2325
2328
|
|
|
2329
|
+
// src/tools/memory.ts
|
|
2330
|
+
function registerMemoryTools(registry, opts = {}) {
|
|
2331
|
+
const store = new MemoryStore({ homeDir: opts.homeDir, projectRoot: opts.projectRoot });
|
|
2332
|
+
const hasProject = store.hasProjectScope();
|
|
2333
|
+
registry.register({
|
|
2334
|
+
name: "remember",
|
|
2335
|
+
description: "Save a memory for future sessions. Use when the user states a preference, corrects your approach, shares a non-obvious fact about this project, or explicitly asks you to remember something. Don't remember transient task state \u2014 only things worth recalling next session. The memory is written now but won't re-load into the system prompt until the next `/new` or launch.",
|
|
2336
|
+
parameters: {
|
|
2337
|
+
type: "object",
|
|
2338
|
+
properties: {
|
|
2339
|
+
type: {
|
|
2340
|
+
type: "string",
|
|
2341
|
+
enum: ["user", "feedback", "project", "reference"],
|
|
2342
|
+
description: "'user' = role/skills/prefs; 'feedback' = corrections or confirmed approaches; 'project' = facts/decisions about the current work; 'reference' = pointers to external systems the user uses."
|
|
2343
|
+
},
|
|
2344
|
+
scope: {
|
|
2345
|
+
type: "string",
|
|
2346
|
+
enum: ["global", "project"],
|
|
2347
|
+
description: "'global' = applies across every project (preferences, tooling); 'project' = scoped to the current sandbox (decisions, local facts). Only available in `reasonix code`."
|
|
2348
|
+
},
|
|
2349
|
+
name: {
|
|
2350
|
+
type: "string",
|
|
2351
|
+
description: "filename-safe identifier, 3-40 chars, alnum + _ - . (no path separators, no leading dot)."
|
|
2352
|
+
},
|
|
2353
|
+
description: {
|
|
2354
|
+
type: "string",
|
|
2355
|
+
description: "One-line summary shown in MEMORY.md (under ~150 chars)."
|
|
2356
|
+
},
|
|
2357
|
+
content: {
|
|
2358
|
+
type: "string",
|
|
2359
|
+
description: "Full memory body in markdown. For feedback/project types, structure as: rule/fact, then **Why:** line, then **How to apply:** line."
|
|
2360
|
+
}
|
|
2361
|
+
},
|
|
2362
|
+
required: ["type", "scope", "name", "description", "content"]
|
|
2363
|
+
},
|
|
2364
|
+
fn: async (args) => {
|
|
2365
|
+
if (args.scope === "project" && !hasProject) {
|
|
2366
|
+
return JSON.stringify({
|
|
2367
|
+
error: "scope='project' is unavailable in this session (no sandbox root). Retry with scope='global', or ask the user to switch to `reasonix code` for project-scoped memory."
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
try {
|
|
2371
|
+
const path = store.write({
|
|
2372
|
+
name: args.name,
|
|
2373
|
+
type: args.type,
|
|
2374
|
+
scope: args.scope,
|
|
2375
|
+
description: args.description,
|
|
2376
|
+
body: args.content
|
|
2377
|
+
});
|
|
2378
|
+
const key = sanitizeMemoryName(args.name);
|
|
2379
|
+
return [
|
|
2380
|
+
`\u2713 REMEMBERED (${args.scope}/${key}): ${args.description}`,
|
|
2381
|
+
"",
|
|
2382
|
+
"TREAT THIS AS ESTABLISHED FACT for the rest of this session.",
|
|
2383
|
+
"The user just told you \u2014 don't re-explore the filesystem to re-derive it.",
|
|
2384
|
+
`(Saved to ${path}; pins into the system prompt on next /new or launch.)`
|
|
2385
|
+
].join("\n");
|
|
2386
|
+
} catch (err) {
|
|
2387
|
+
return JSON.stringify({ error: `remember failed: ${err.message}` });
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
});
|
|
2391
|
+
registry.register({
|
|
2392
|
+
name: "forget",
|
|
2393
|
+
description: "Delete a memory file and remove it from MEMORY.md. Use when the user explicitly asks to forget something, or when a previously-remembered fact has become wrong. Irreversible \u2014 no tombstone.",
|
|
2394
|
+
parameters: {
|
|
2395
|
+
type: "object",
|
|
2396
|
+
properties: {
|
|
2397
|
+
name: { type: "string", description: "Memory name (the identifier used in `remember`)." },
|
|
2398
|
+
scope: { type: "string", enum: ["global", "project"] }
|
|
2399
|
+
},
|
|
2400
|
+
required: ["name", "scope"]
|
|
2401
|
+
},
|
|
2402
|
+
fn: async (args) => {
|
|
2403
|
+
if (args.scope === "project" && !hasProject) {
|
|
2404
|
+
return JSON.stringify({
|
|
2405
|
+
error: "scope='project' is unavailable in this session (no sandbox root)."
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
try {
|
|
2409
|
+
const existed = store.delete(args.scope, args.name);
|
|
2410
|
+
return existed ? `forgot (${args.scope}/${sanitizeMemoryName(args.name)}). Re-load on next /new or launch.` : `no such memory: ${args.scope}/${args.name} (nothing to forget).`;
|
|
2411
|
+
} catch (err) {
|
|
2412
|
+
return JSON.stringify({ error: `forget failed: ${err.message}` });
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
});
|
|
2416
|
+
registry.register({
|
|
2417
|
+
name: "recall_memory",
|
|
2418
|
+
description: "Read the full body of a memory file when its MEMORY.md one-liner (already in the system prompt) isn't enough detail. Most of the time the index suffices \u2014 only call this when the user's question genuinely requires the full context.",
|
|
2419
|
+
readOnly: true,
|
|
2420
|
+
parameters: {
|
|
2421
|
+
type: "object",
|
|
2422
|
+
properties: {
|
|
2423
|
+
name: { type: "string" },
|
|
2424
|
+
scope: { type: "string", enum: ["global", "project"] }
|
|
2425
|
+
},
|
|
2426
|
+
required: ["name", "scope"]
|
|
2427
|
+
},
|
|
2428
|
+
fn: async (args) => {
|
|
2429
|
+
if (args.scope === "project" && !hasProject) {
|
|
2430
|
+
return JSON.stringify({
|
|
2431
|
+
error: "scope='project' is unavailable in this session (no sandbox root)."
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
try {
|
|
2435
|
+
const entry = store.read(args.scope, args.name);
|
|
2436
|
+
return [
|
|
2437
|
+
`# ${entry.name} (${entry.scope}/${entry.type}, created ${entry.createdAt || "?"})`,
|
|
2438
|
+
entry.description ? `> ${entry.description}` : "",
|
|
2439
|
+
"",
|
|
2440
|
+
entry.body
|
|
2441
|
+
].filter(Boolean).join("\n");
|
|
2442
|
+
} catch (err) {
|
|
2443
|
+
return JSON.stringify({ error: `recall failed: ${err.message}` });
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
});
|
|
2447
|
+
return registry;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2326
2450
|
// src/tools/plan.ts
|
|
2327
2451
|
var PlanProposedError = class extends Error {
|
|
2328
2452
|
plan;
|
|
@@ -2540,7 +2664,7 @@ function resolveExecutable(cmd, opts = {}) {
|
|
|
2540
2664
|
const isFile = opts.isFile ?? defaultIsFile;
|
|
2541
2665
|
for (const dir of pathDirs) {
|
|
2542
2666
|
for (const ext of pathExt) {
|
|
2543
|
-
const full = pathMod2.join(dir, cmd + ext);
|
|
2667
|
+
const full = pathMod2.win32.join(dir, cmd + ext);
|
|
2544
2668
|
if (isFile(full)) return full;
|
|
2545
2669
|
}
|
|
2546
2670
|
}
|
|
@@ -2573,8 +2697,23 @@ function prepareSpawn(argv, opts = {}) {
|
|
|
2573
2697
|
spawnOverrides: { windowsVerbatimArguments: true }
|
|
2574
2698
|
};
|
|
2575
2699
|
}
|
|
2700
|
+
if (isBareWindowsName(resolved) && resolved === head) {
|
|
2701
|
+
const cmdline = [head, ...tail].map(quoteForCmdExe).join(" ");
|
|
2702
|
+
return {
|
|
2703
|
+
bin: "cmd.exe",
|
|
2704
|
+
args: ["/d", "/s", "/c", cmdline],
|
|
2705
|
+
spawnOverrides: { windowsVerbatimArguments: true }
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2576
2708
|
return { bin: resolved, args: [...tail], spawnOverrides: {} };
|
|
2577
2709
|
}
|
|
2710
|
+
function isBareWindowsName(s) {
|
|
2711
|
+
if (!s) return false;
|
|
2712
|
+
if (s.includes("/") || s.includes("\\")) return false;
|
|
2713
|
+
if (pathMod2.isAbsolute(s)) return false;
|
|
2714
|
+
if (pathMod2.extname(s)) return false;
|
|
2715
|
+
return true;
|
|
2716
|
+
}
|
|
2578
2717
|
function quoteForCmdExe(arg) {
|
|
2579
2718
|
if (arg === "") return '""';
|
|
2580
2719
|
if (!/[\s"&|<>^%(),;!]/.test(arg)) return arg;
|
|
@@ -2594,11 +2733,14 @@ function registerShellTools(registry, opts) {
|
|
|
2594
2733
|
const rootDir = pathMod2.resolve(opts.rootDir);
|
|
2595
2734
|
const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC;
|
|
2596
2735
|
const maxOutputChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
2597
|
-
const
|
|
2736
|
+
const getExtraAllowed = typeof opts.extraAllowed === "function" ? opts.extraAllowed : (() => {
|
|
2737
|
+
const snapshot = opts.extraAllowed ?? [];
|
|
2738
|
+
return () => snapshot;
|
|
2739
|
+
})();
|
|
2598
2740
|
const allowAll = opts.allowAll ?? false;
|
|
2599
2741
|
registry.register({
|
|
2600
2742
|
name: "run_command",
|
|
2601
|
-
description: "Run a shell command in the project root and return its combined stdout+stderr.
|
|
2743
|
+
description: "Run a shell command in the project root and return its combined stdout+stderr. Common read-only inspection and test/lint/typecheck commands run immediately; anything that could mutate state, install dependencies, or touch the network is refused until the user confirms it in the TUI. Prefer this over asking the user to run a command manually \u2014 after edits, run the project's tests to verify.",
|
|
2602
2744
|
// Plan-mode gate: allow allowlisted commands through (git status,
|
|
2603
2745
|
// cargo check, ls, grep …) so the model can actually investigate
|
|
2604
2746
|
// during planning. Anything that would otherwise trigger a
|
|
@@ -2607,14 +2749,14 @@ function registerShellTools(registry, opts) {
|
|
|
2607
2749
|
if (allowAll) return true;
|
|
2608
2750
|
const cmd = typeof args?.command === "string" ? args.command.trim() : "";
|
|
2609
2751
|
if (!cmd) return false;
|
|
2610
|
-
return isAllowed(cmd,
|
|
2752
|
+
return isAllowed(cmd, getExtraAllowed());
|
|
2611
2753
|
},
|
|
2612
2754
|
parameters: {
|
|
2613
2755
|
type: "object",
|
|
2614
2756
|
properties: {
|
|
2615
2757
|
command: {
|
|
2616
2758
|
type: "string",
|
|
2617
|
-
description: "Full command line
|
|
2759
|
+
description: "Full command line. Tokenized with POSIX-ish quoting; no shell expansion, no pipes, no redirects."
|
|
2618
2760
|
},
|
|
2619
2761
|
timeoutSec: {
|
|
2620
2762
|
type: "integer",
|
|
@@ -2626,7 +2768,7 @@ function registerShellTools(registry, opts) {
|
|
|
2626
2768
|
fn: async (args, ctx) => {
|
|
2627
2769
|
const cmd = args.command.trim();
|
|
2628
2770
|
if (!cmd) throw new Error("run_command: empty command");
|
|
2629
|
-
if (!allowAll && !isAllowed(cmd,
|
|
2771
|
+
if (!allowAll && !isAllowed(cmd, getExtraAllowed())) {
|
|
2630
2772
|
throw new NeedsConfirmationError(cmd);
|
|
2631
2773
|
}
|
|
2632
2774
|
const effectiveTimeout = Math.max(1, Math.min(600, args.timeoutSec ?? timeoutSec));
|
|
@@ -4119,13 +4261,64 @@ function sep() {
|
|
|
4119
4261
|
}
|
|
4120
4262
|
|
|
4121
4263
|
// src/index.ts
|
|
4122
|
-
var VERSION = "0.4.
|
|
4264
|
+
var VERSION = "0.4.20";
|
|
4123
4265
|
|
|
4124
4266
|
// src/cli/commands/chat.tsx
|
|
4125
4267
|
import { existsSync as existsSync4, statSync as statSync3 } from "fs";
|
|
4126
4268
|
import { render } from "ink";
|
|
4127
4269
|
import React15, { useState as useState7 } from "react";
|
|
4128
4270
|
|
|
4271
|
+
// src/tools/skills.ts
|
|
4272
|
+
function registerSkillTools(registry, opts = {}) {
|
|
4273
|
+
const store = new SkillStore({ homeDir: opts.homeDir, projectRoot: opts.projectRoot });
|
|
4274
|
+
registry.register({
|
|
4275
|
+
name: "run_skill",
|
|
4276
|
+
description: "Load the full body of a user-defined skill into this conversation. Call when the pinned Skills index (in the system prompt) lists a skill whose description matches what's being asked. Returns the skill's markdown instructions \u2014 read them and continue the loop, calling whatever filesystem / shell / web tools the skill's prose requires. Skills are user content; follow their instructions, but keep Reasonix's own safety rules (no destructive ops without confirmation, etc.).",
|
|
4277
|
+
readOnly: true,
|
|
4278
|
+
parameters: {
|
|
4279
|
+
type: "object",
|
|
4280
|
+
properties: {
|
|
4281
|
+
name: {
|
|
4282
|
+
type: "string",
|
|
4283
|
+
description: "Skill identifier as it appears in the pinned Skills index (e.g. 'review', 'security-review'). Case-sensitive."
|
|
4284
|
+
},
|
|
4285
|
+
arguments: {
|
|
4286
|
+
type: "string",
|
|
4287
|
+
description: "Optional free-form arguments the caller wants the skill to act on. Forwarded verbatim as an 'Arguments:' line appended to the skill body; the skill's own instructions decide how to consume them."
|
|
4288
|
+
}
|
|
4289
|
+
},
|
|
4290
|
+
required: ["name"]
|
|
4291
|
+
},
|
|
4292
|
+
fn: async (args) => {
|
|
4293
|
+
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
4294
|
+
if (!name) {
|
|
4295
|
+
return JSON.stringify({ error: "run_skill requires a 'name' argument" });
|
|
4296
|
+
}
|
|
4297
|
+
const skill = store.read(name);
|
|
4298
|
+
if (!skill) {
|
|
4299
|
+
const available = store.list().map((s) => s.name).join(", ");
|
|
4300
|
+
return JSON.stringify({
|
|
4301
|
+
error: `unknown skill: ${JSON.stringify(name)}`,
|
|
4302
|
+
available: available || "(none \u2014 user has not defined any skills)"
|
|
4303
|
+
});
|
|
4304
|
+
}
|
|
4305
|
+
const rawArgs = typeof args.arguments === "string" ? args.arguments.trim() : "";
|
|
4306
|
+
const header = [
|
|
4307
|
+
`# Skill: ${skill.name}`,
|
|
4308
|
+
skill.description ? `> ${skill.description}` : "",
|
|
4309
|
+
`(scope: ${skill.scope} \xB7 ${skill.path})`
|
|
4310
|
+
].filter(Boolean).join("\n");
|
|
4311
|
+
const argsBlock = rawArgs ? `
|
|
4312
|
+
|
|
4313
|
+
Arguments: ${rawArgs}` : "";
|
|
4314
|
+
return `${header}
|
|
4315
|
+
|
|
4316
|
+
${skill.body}${argsBlock}`;
|
|
4317
|
+
}
|
|
4318
|
+
});
|
|
4319
|
+
return registry;
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4129
4322
|
// src/cli/ui/App.tsx
|
|
4130
4323
|
import { Box as Box11, Static, Text as Text11, useApp, useInput as useInput4 } from "ink";
|
|
4131
4324
|
import React12, { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState5 } from "react";
|
|
@@ -5093,7 +5286,16 @@ var SLASH_COMMANDS = [
|
|
|
5093
5286
|
{ cmd: "branch", argsHint: "<N|off>", summary: "run N parallel samples per turn (N>=2)" },
|
|
5094
5287
|
{ cmd: "mcp", summary: "list MCP servers + tools attached to this session" },
|
|
5095
5288
|
{ cmd: "tool", argsHint: "[N]", summary: "dump full output of the Nth tool call (1=latest)" },
|
|
5096
|
-
{
|
|
5289
|
+
{
|
|
5290
|
+
cmd: "memory",
|
|
5291
|
+
argsHint: "[list|show <name>|forget <name>|clear <scope> confirm]",
|
|
5292
|
+
summary: "show / manage pinned memory (REASONIX.md + ~/.reasonix/memory)"
|
|
5293
|
+
},
|
|
5294
|
+
{
|
|
5295
|
+
cmd: "skill",
|
|
5296
|
+
argsHint: "[list|show <name>|<name> [args]]",
|
|
5297
|
+
summary: "list / run user skills (<project>/.reasonix/skills + ~/.reasonix/skills)"
|
|
5298
|
+
},
|
|
5097
5299
|
{ cmd: "think", summary: "dump the last turn's full R1 reasoning (reasoner only)" },
|
|
5098
5300
|
{ cmd: "retry", summary: "truncate & resend your last message (fresh sample)" },
|
|
5099
5301
|
{ cmd: "compact", argsHint: "[cap]", summary: "shrink oversized tool results in the log" },
|
|
@@ -5173,7 +5375,10 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
5173
5375
|
" /compact [cap] shrink large tool results in history (default 4k/result)",
|
|
5174
5376
|
" /think dump the most recent turn's full R1 reasoning (reasoner only)",
|
|
5175
5377
|
" /tool [N] list tool calls (or dump full output of #N, 1=most recent)",
|
|
5176
|
-
" /memory
|
|
5378
|
+
" /memory [sub] show pinned memory (REASONIX.md + ~/.reasonix/memory).",
|
|
5379
|
+
" subs: list | show <name> | forget <name> | clear <scope> confirm",
|
|
5380
|
+
" /skill [sub] list / run user skills (project/.reasonix/skills + ~/.reasonix/skills).",
|
|
5381
|
+
" subs: list | show <name> | <name> [args] (injects skill body as user turn)",
|
|
5177
5382
|
" /retry truncate & resend your last message (fresh sample from the model)",
|
|
5178
5383
|
" /apply (code mode) commit the pending edit blocks to disk",
|
|
5179
5384
|
" /discard (code mode) drop pending edits without writing",
|
|
@@ -5258,43 +5463,11 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
5258
5463
|
};
|
|
5259
5464
|
}
|
|
5260
5465
|
case "memory": {
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
if (!ctx.memoryRoot) {
|
|
5267
|
-
return {
|
|
5268
|
-
info: "no project root on this session \u2014 `/memory` needs a working directory to resolve REASONIX.md from."
|
|
5269
|
-
};
|
|
5270
|
-
}
|
|
5271
|
-
const mem = readProjectMemory(ctx.memoryRoot);
|
|
5272
|
-
if (!mem) {
|
|
5273
|
-
return {
|
|
5274
|
-
info: [
|
|
5275
|
-
`no ${PROJECT_MEMORY_FILE} in ${ctx.memoryRoot}.`,
|
|
5276
|
-
"",
|
|
5277
|
-
"Project memory is an optional file you pin notes into \u2014 project conventions,",
|
|
5278
|
-
"things the model keeps forgetting, domain glossary, setup gotchas. When present,",
|
|
5279
|
-
"its contents are appended to the system prompt (the immutable-prefix region)",
|
|
5280
|
-
"so every turn sees it without eating per-turn context, and the prefix cache stays",
|
|
5281
|
-
"warm as long as the file is stable.",
|
|
5282
|
-
"",
|
|
5283
|
-
`Create it with: echo "# Project notes for Reasonix" > ${PROJECT_MEMORY_FILE}`,
|
|
5284
|
-
"Re-launch (or `/new`) to pick up changes \u2014 the prefix is hashed at session start."
|
|
5285
|
-
].join("\n")
|
|
5286
|
-
};
|
|
5287
|
-
}
|
|
5288
|
-
const header = mem.truncated ? `\u25B8 project memory: ${mem.path} (${mem.originalChars.toLocaleString()} chars, truncated for the prefix)` : `\u25B8 project memory: ${mem.path} (${mem.originalChars.toLocaleString()} chars)`;
|
|
5289
|
-
return {
|
|
5290
|
-
info: [
|
|
5291
|
-
header,
|
|
5292
|
-
"",
|
|
5293
|
-
mem.content,
|
|
5294
|
-
"",
|
|
5295
|
-
"Changes take effect on the next launch or `/new` \u2014 the system prompt is hashed once per session to keep the prefix cache warm."
|
|
5296
|
-
].join("\n")
|
|
5297
|
-
};
|
|
5466
|
+
return handleMemorySlash(args, ctx);
|
|
5467
|
+
}
|
|
5468
|
+
case "skill":
|
|
5469
|
+
case "skills": {
|
|
5470
|
+
return handleSkillSlash(args, ctx);
|
|
5298
5471
|
}
|
|
5299
5472
|
case "think":
|
|
5300
5473
|
case "reasoning": {
|
|
@@ -5532,6 +5705,228 @@ ${entry.text}`
|
|
|
5532
5705
|
return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
|
|
5533
5706
|
}
|
|
5534
5707
|
}
|
|
5708
|
+
function handleSkillSlash(args, ctx) {
|
|
5709
|
+
const store = new SkillStore({ projectRoot: ctx.codeRoot });
|
|
5710
|
+
const sub = (args[0] ?? "").toLowerCase();
|
|
5711
|
+
if (sub === "" || sub === "list" || sub === "ls") {
|
|
5712
|
+
const skills = store.list();
|
|
5713
|
+
if (skills.length === 0) {
|
|
5714
|
+
const lines2 = ["no skills found. Reasonix reads skills from:"];
|
|
5715
|
+
if (store.hasProjectScope()) {
|
|
5716
|
+
lines2.push(
|
|
5717
|
+
" \xB7 <project>/.reasonix/skills/<name>/SKILL.md (or <name>.md) \u2014 project scope"
|
|
5718
|
+
);
|
|
5719
|
+
}
|
|
5720
|
+
lines2.push(" \xB7 ~/.reasonix/skills/<name>/SKILL.md (or <name>.md) \u2014 global scope");
|
|
5721
|
+
if (!store.hasProjectScope()) {
|
|
5722
|
+
lines2.push(" (project scope is only active in `reasonix code`)");
|
|
5723
|
+
}
|
|
5724
|
+
lines2.push(
|
|
5725
|
+
"",
|
|
5726
|
+
"Each file's frontmatter needs at least `name` and `description`.",
|
|
5727
|
+
"Invoke a skill with `/skill <name> [args]` or by asking the model to call `run_skill`."
|
|
5728
|
+
);
|
|
5729
|
+
return { info: lines2.join("\n") };
|
|
5730
|
+
}
|
|
5731
|
+
const lines = [`User skills (${skills.length}):`];
|
|
5732
|
+
for (const s of skills) {
|
|
5733
|
+
const scope = `(${s.scope})`.padEnd(11);
|
|
5734
|
+
const name2 = s.name.padEnd(24);
|
|
5735
|
+
const desc = s.description.length > 70 ? `${s.description.slice(0, 69)}\u2026` : s.description;
|
|
5736
|
+
lines.push(` ${scope} ${name2} ${desc}`);
|
|
5737
|
+
}
|
|
5738
|
+
lines.push("");
|
|
5739
|
+
lines.push("View body: /skill show <name> Run: /skill <name> [args]");
|
|
5740
|
+
return { info: lines.join("\n") };
|
|
5741
|
+
}
|
|
5742
|
+
if (sub === "show" || sub === "cat") {
|
|
5743
|
+
const target = args[1];
|
|
5744
|
+
if (!target) return { info: "usage: /skill show <name>" };
|
|
5745
|
+
const skill2 = store.read(target);
|
|
5746
|
+
if (!skill2) return { info: `no skill found: ${target}` };
|
|
5747
|
+
return {
|
|
5748
|
+
info: [
|
|
5749
|
+
`\u25B8 ${skill2.name} (${skill2.scope})`,
|
|
5750
|
+
skill2.description ? ` ${skill2.description}` : "",
|
|
5751
|
+
` ${skill2.path}`,
|
|
5752
|
+
"",
|
|
5753
|
+
skill2.body
|
|
5754
|
+
].filter((l) => l !== "").join("\n")
|
|
5755
|
+
};
|
|
5756
|
+
}
|
|
5757
|
+
const name = args[0] ?? "";
|
|
5758
|
+
const skill = store.read(name);
|
|
5759
|
+
if (!skill) {
|
|
5760
|
+
return {
|
|
5761
|
+
info: `no skill found: ${name} (try /skill list)`
|
|
5762
|
+
};
|
|
5763
|
+
}
|
|
5764
|
+
const extra = args.slice(1).join(" ").trim();
|
|
5765
|
+
const header = `# Skill: ${skill.name}${skill.description ? `
|
|
5766
|
+
> ${skill.description}` : ""}`;
|
|
5767
|
+
const argsLine = extra ? `
|
|
5768
|
+
|
|
5769
|
+
Arguments: ${extra}` : "";
|
|
5770
|
+
const payload = `${header}
|
|
5771
|
+
|
|
5772
|
+
${skill.body}${argsLine}`;
|
|
5773
|
+
return {
|
|
5774
|
+
info: `\u25B8 running skill: ${skill.name}${extra ? ` \u2014 ${extra}` : ""}`,
|
|
5775
|
+
resubmit: payload
|
|
5776
|
+
};
|
|
5777
|
+
}
|
|
5778
|
+
function handleMemorySlash(args, ctx) {
|
|
5779
|
+
if (!memoryEnabled()) {
|
|
5780
|
+
return {
|
|
5781
|
+
info: "memory is disabled (REASONIX_MEMORY=off in env). Unset the var to re-enable \u2014 no REASONIX.md or ~/.reasonix/memory content will be pinned in the meantime."
|
|
5782
|
+
};
|
|
5783
|
+
}
|
|
5784
|
+
if (!ctx.memoryRoot) {
|
|
5785
|
+
return {
|
|
5786
|
+
info: "no working directory on this session \u2014 `/memory` needs a root to resolve REASONIX.md from. (Running in a test harness?)"
|
|
5787
|
+
};
|
|
5788
|
+
}
|
|
5789
|
+
const store = new MemoryStore({ projectRoot: ctx.codeRoot });
|
|
5790
|
+
const sub = (args[0] ?? "").toLowerCase();
|
|
5791
|
+
if (sub === "list" || sub === "ls") {
|
|
5792
|
+
const entries = store.list();
|
|
5793
|
+
if (entries.length === 0) {
|
|
5794
|
+
return {
|
|
5795
|
+
info: "no user memories yet. The model can call `remember` to save one, or you can create files by hand in ~/.reasonix/memory/global/ or the per-project subdir."
|
|
5796
|
+
};
|
|
5797
|
+
}
|
|
5798
|
+
const lines = [`User memories (${entries.length}):`];
|
|
5799
|
+
for (const e of entries) {
|
|
5800
|
+
const tag = `${e.scope}/${e.type}`.padEnd(18);
|
|
5801
|
+
const name = e.name.padEnd(28);
|
|
5802
|
+
const desc = e.description.length > 70 ? `${e.description.slice(0, 69)}\u2026` : e.description;
|
|
5803
|
+
lines.push(` ${tag} ${name} ${desc}`);
|
|
5804
|
+
}
|
|
5805
|
+
lines.push("");
|
|
5806
|
+
lines.push("View body: /memory show <name> Delete: /memory forget <name>");
|
|
5807
|
+
return { info: lines.join("\n") };
|
|
5808
|
+
}
|
|
5809
|
+
if (sub === "show" || sub === "cat") {
|
|
5810
|
+
const target = args[1];
|
|
5811
|
+
if (!target) return { info: "usage: /memory show <name> or /memory show <scope>/<name>" };
|
|
5812
|
+
const resolved = resolveMemoryTarget(store, target);
|
|
5813
|
+
if (!resolved) return { info: `no memory found: ${target}` };
|
|
5814
|
+
try {
|
|
5815
|
+
const entry = store.read(resolved.scope, resolved.name);
|
|
5816
|
+
return {
|
|
5817
|
+
info: [
|
|
5818
|
+
`\u25B8 ${entry.scope}/${entry.name} (${entry.type}, created ${entry.createdAt || "?"})`,
|
|
5819
|
+
entry.description ? ` ${entry.description}` : "",
|
|
5820
|
+
"",
|
|
5821
|
+
entry.body
|
|
5822
|
+
].filter((l) => l !== "").concat("").join("\n")
|
|
5823
|
+
};
|
|
5824
|
+
} catch (err) {
|
|
5825
|
+
return { info: `show failed: ${err.message}` };
|
|
5826
|
+
}
|
|
5827
|
+
}
|
|
5828
|
+
if (sub === "forget" || sub === "rm" || sub === "delete") {
|
|
5829
|
+
const target = args[1];
|
|
5830
|
+
if (!target) return { info: "usage: /memory forget <name> or /memory forget <scope>/<name>" };
|
|
5831
|
+
const resolved = resolveMemoryTarget(store, target);
|
|
5832
|
+
if (!resolved) return { info: `no memory found: ${target}` };
|
|
5833
|
+
try {
|
|
5834
|
+
const ok = store.delete(resolved.scope, resolved.name);
|
|
5835
|
+
return {
|
|
5836
|
+
info: ok ? `\u25B8 forgot ${resolved.scope}/${resolved.name}. Next /new or launch won't see it.` : `could not forget ${resolved.scope}/${resolved.name} (already gone?)`
|
|
5837
|
+
};
|
|
5838
|
+
} catch (err) {
|
|
5839
|
+
return { info: `forget failed: ${err.message}` };
|
|
5840
|
+
}
|
|
5841
|
+
}
|
|
5842
|
+
if (sub === "clear") {
|
|
5843
|
+
const rawScope = (args[1] ?? "").toLowerCase();
|
|
5844
|
+
if (rawScope !== "global" && rawScope !== "project") {
|
|
5845
|
+
return { info: "usage: /memory clear <global|project> confirm" };
|
|
5846
|
+
}
|
|
5847
|
+
if ((args[2] ?? "").toLowerCase() !== "confirm") {
|
|
5848
|
+
return {
|
|
5849
|
+
info: `about to delete every memory in scope=${rawScope}. Re-run with the word 'confirm' to proceed: /memory clear ${rawScope} confirm`
|
|
5850
|
+
};
|
|
5851
|
+
}
|
|
5852
|
+
const scope = rawScope;
|
|
5853
|
+
const entries = store.list().filter((e) => e.scope === scope);
|
|
5854
|
+
let deleted = 0;
|
|
5855
|
+
for (const e of entries) {
|
|
5856
|
+
try {
|
|
5857
|
+
if (store.delete(scope, e.name)) deleted++;
|
|
5858
|
+
} catch {
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
return { info: `\u25B8 cleared scope=${scope} \u2014 deleted ${deleted} memory file(s).` };
|
|
5862
|
+
}
|
|
5863
|
+
const parts = [];
|
|
5864
|
+
const projMem = readProjectMemory(ctx.memoryRoot);
|
|
5865
|
+
if (projMem) {
|
|
5866
|
+
const hdr = projMem.truncated ? `\u25B8 ${PROJECT_MEMORY_FILE}: ${projMem.path} (${projMem.originalChars.toLocaleString()} chars, truncated)` : `\u25B8 ${PROJECT_MEMORY_FILE}: ${projMem.path} (${projMem.originalChars.toLocaleString()} chars)`;
|
|
5867
|
+
parts.push(hdr, "", projMem.content);
|
|
5868
|
+
}
|
|
5869
|
+
const globalIdx = store.loadIndex("global");
|
|
5870
|
+
if (globalIdx) {
|
|
5871
|
+
parts.push(
|
|
5872
|
+
"",
|
|
5873
|
+
`\u25B8 global memory (${globalIdx.originalChars.toLocaleString()} chars${globalIdx.truncated ? ", truncated" : ""})`,
|
|
5874
|
+
"",
|
|
5875
|
+
globalIdx.content
|
|
5876
|
+
);
|
|
5877
|
+
}
|
|
5878
|
+
const projectIdx = store.loadIndex("project");
|
|
5879
|
+
if (projectIdx) {
|
|
5880
|
+
parts.push(
|
|
5881
|
+
"",
|
|
5882
|
+
`\u25B8 project memory (${projectIdx.originalChars.toLocaleString()} chars${projectIdx.truncated ? ", truncated" : ""})`,
|
|
5883
|
+
"",
|
|
5884
|
+
projectIdx.content
|
|
5885
|
+
);
|
|
5886
|
+
}
|
|
5887
|
+
if (parts.length === 0) {
|
|
5888
|
+
return {
|
|
5889
|
+
info: [
|
|
5890
|
+
`no memory pinned in ${ctx.memoryRoot}.`,
|
|
5891
|
+
"",
|
|
5892
|
+
"Three layers are available:",
|
|
5893
|
+
` 1. ${PROJECT_MEMORY_FILE} \u2014 committable team memory (in the repo).`,
|
|
5894
|
+
" 2. ~/.reasonix/memory/global/ \u2014 your cross-project private memory.",
|
|
5895
|
+
` 3. ~/.reasonix/memory/<project-hash>/ \u2014 this project's private memory.`,
|
|
5896
|
+
"",
|
|
5897
|
+
"Ask the model to `remember` something, or hand-edit files directly.",
|
|
5898
|
+
"Changes take effect on next /new or launch \u2014 the system prompt is hashed once per session to keep the prefix cache warm.",
|
|
5899
|
+
"",
|
|
5900
|
+
"Subcommands: /memory list | /memory show <name> | /memory forget <name> | /memory clear <scope> confirm"
|
|
5901
|
+
].join("\n")
|
|
5902
|
+
};
|
|
5903
|
+
}
|
|
5904
|
+
parts.push(
|
|
5905
|
+
"",
|
|
5906
|
+
"Changes take effect on next /new or launch. Subcommands: /memory list | show | forget | clear"
|
|
5907
|
+
);
|
|
5908
|
+
return { info: parts.join("\n") };
|
|
5909
|
+
}
|
|
5910
|
+
function resolveMemoryTarget(store, raw) {
|
|
5911
|
+
const slash = raw.indexOf("/");
|
|
5912
|
+
if (slash > 0) {
|
|
5913
|
+
const scopeRaw = raw.slice(0, slash).toLowerCase();
|
|
5914
|
+
const name = raw.slice(slash + 1);
|
|
5915
|
+
if (scopeRaw !== "global" && scopeRaw !== "project") return null;
|
|
5916
|
+
const scope = scopeRaw;
|
|
5917
|
+
if (scope === "project" && !store.hasProjectScope()) return null;
|
|
5918
|
+
return { scope, name };
|
|
5919
|
+
}
|
|
5920
|
+
for (const scope of ["project", "global"]) {
|
|
5921
|
+
if (scope === "project" && !store.hasProjectScope()) continue;
|
|
5922
|
+
try {
|
|
5923
|
+
store.read(scope, raw);
|
|
5924
|
+
return { scope, name: raw };
|
|
5925
|
+
} catch {
|
|
5926
|
+
}
|
|
5927
|
+
}
|
|
5928
|
+
return null;
|
|
5929
|
+
}
|
|
5535
5930
|
function appendSection(lines, label, section) {
|
|
5536
5931
|
if (!section || !section.supported) {
|
|
5537
5932
|
lines.push(
|
|
@@ -6611,6 +7006,11 @@ async function chatCommand(opts) {
|
|
|
6611
7006
|
if (!tools) tools = new ToolRegistry();
|
|
6612
7007
|
registerWebTools(tools);
|
|
6613
7008
|
}
|
|
7009
|
+
if (!opts.seedTools) {
|
|
7010
|
+
if (!tools) tools = new ToolRegistry();
|
|
7011
|
+
registerMemoryTools(tools, {});
|
|
7012
|
+
registerSkillTools(tools);
|
|
7013
|
+
}
|
|
6614
7014
|
let sessionPreview;
|
|
6615
7015
|
if (opts.session && !opts.forceResume && !opts.forceNew) {
|
|
6616
7016
|
const prior = loadSessionMessages(opts.session);
|
|
@@ -6649,7 +7049,7 @@ async function chatCommand(opts) {
|
|
|
6649
7049
|
// src/cli/commands/code.tsx
|
|
6650
7050
|
import { basename, resolve as resolve5 } from "path";
|
|
6651
7051
|
async function codeCommand(opts = {}) {
|
|
6652
|
-
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-
|
|
7052
|
+
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-VDN5U3YE.js");
|
|
6653
7053
|
const rootDir = resolve5(opts.dir ?? process.cwd());
|
|
6654
7054
|
const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
|
|
6655
7055
|
const tools = new ToolRegistry();
|
|
@@ -6658,9 +7058,14 @@ async function codeCommand(opts = {}) {
|
|
|
6658
7058
|
rootDir,
|
|
6659
7059
|
// Per-project "always allow" list persisted from prior ShellConfirm
|
|
6660
7060
|
// choices; merged on top of the built-in allowlist in shell.ts.
|
|
6661
|
-
|
|
7061
|
+
// GETTER form — re-read every dispatch so a prefix the user adds
|
|
7062
|
+
// via ShellConfirm mid-session takes effect on the next shell call
|
|
7063
|
+
// instead of waiting for `/new` or a relaunch.
|
|
7064
|
+
extraAllowed: () => loadProjectShellAllowed(rootDir)
|
|
6662
7065
|
});
|
|
6663
7066
|
registerPlanTool(tools);
|
|
7067
|
+
registerMemoryTools(tools, { projectRoot: rootDir });
|
|
7068
|
+
registerSkillTools(tools, { projectRoot: rootDir });
|
|
6664
7069
|
process.stderr.write(
|
|
6665
7070
|
`\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)
|
|
6666
7071
|
`
|
|
@@ -7710,7 +8115,7 @@ program.action(async () => {
|
|
|
7710
8115
|
const defaults = resolveDefaults({});
|
|
7711
8116
|
await chatCommand({
|
|
7712
8117
|
model: defaults.model,
|
|
7713
|
-
system:
|
|
8118
|
+
system: applyMemoryStack(DEFAULT_SYSTEM, process.cwd()),
|
|
7714
8119
|
harvest: defaults.harvest,
|
|
7715
8120
|
branch: defaults.branch,
|
|
7716
8121
|
session: defaults.session,
|
|
@@ -7762,7 +8167,7 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
|
|
|
7762
8167
|
});
|
|
7763
8168
|
await chatCommand({
|
|
7764
8169
|
model: defaults.model,
|
|
7765
|
-
system:
|
|
8170
|
+
system: applyMemoryStack(opts.system, process.cwd()),
|
|
7766
8171
|
transcript: opts.transcript,
|
|
7767
8172
|
harvest: defaults.harvest,
|
|
7768
8173
|
branch: defaults.branch,
|
|
@@ -7797,7 +8202,7 @@ program.command("run <task>").description("Run a single task non-interactively,
|
|
|
7797
8202
|
await runCommand2({
|
|
7798
8203
|
task,
|
|
7799
8204
|
model: defaults.model,
|
|
7800
|
-
system:
|
|
8205
|
+
system: applyMemoryStack(opts.system, process.cwd()),
|
|
7801
8206
|
harvest: defaults.harvest,
|
|
7802
8207
|
branch: defaults.branch,
|
|
7803
8208
|
transcript: opts.transcript,
|