@pencil-agent/nano-pencil 1.13.4 → 1.13.6
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/dist/build-meta.json +6 -0
- package/dist/extensions/defaults/CLAUDE.md +5 -5
- package/dist/extensions/defaults/debug/collectors.d.ts +11 -0
- package/dist/extensions/defaults/debug/collectors.js +67 -0
- package/dist/extensions/defaults/debug/index.d.ts +2 -2
- package/dist/extensions/defaults/debug/index.js +90 -36
- package/dist/extensions/defaults/presence/index.d.ts +47 -0
- package/dist/extensions/defaults/presence/index.js +44 -19
- package/dist/extensions/defaults/sal/README.md +4 -0
- package/dist/extensions/defaults/sal/eval/insforge-sink.d.ts +2 -1
- package/dist/extensions/defaults/sal/eval/insforge-sink.js +30 -1
- package/dist/extensions/defaults/sal/eval/types.d.ts +1 -1
- package/dist/extensions/defaults/sal/index.d.ts +38 -4
- package/dist/extensions/defaults/sal/index.js +315 -15
- package/dist/modes/interactive/interactive-mode.js +29 -6
- package/dist/modes/interactive/services/tips.js +30 -11
- package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +230 -57
- package/dist/node_modules/@pencil-agent/ai/models.generated.js +270 -99
- package/package.json +3 -2
- package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/200/273/347/273/223.md" +0 -251
- package/docs/loop /351/207/215/346/236/204/345/256/214/346/210/220/346/212/245/345/221/212.md" +0 -123
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210.md" +0 -1222
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/256/236/347/216/260/346/212/245/345/221/212.md" +0 -158
- package/docs/loop /351/207/215/346/236/204/346/226/271/346/241/210/345/257/271/346/257/224/345/210/206/346/236/220.md" +0 -128
- package/docs/loop /351/207/215/346/236/204/350/256/241/345/210/222.md" +0 -321
- package/docs/loop-usage-examples.md +0 -215
- package/docs/planmode.md +0 -1987
|
@@ -30,7 +30,7 @@ loop/scheduler-parser.ts: Loop command parsing with flags/subcommands, parseSche
|
|
|
30
30
|
loop/scheduler-types.ts: Scheduled loop types, LoopPayloadKind/ScheduledLoopTask/LoopStartSpec/ParsedSchedulerCommand
|
|
31
31
|
loop/README.md: Loop extension documentation - recurring scheduler usage and flags
|
|
32
32
|
btw/index.ts: BTW extension entry - /btw command for quick side questions without interrupting main task, uses completeSimple() for lightweight response, BTW_MESSAGE_TYPE renderer
|
|
33
|
-
debug/index.ts: Debug extension entry - /debug command
|
|
33
|
+
debug/index.ts: Debug extension entry - /debug command dispatches diagnostics through full agent loop (sendUserMessage + before_agent_start hook), three-layer analysis (Phenomenon/Essence/Philosophy), supports /debug env|session|model quick subcommands, DEBUG_MESSAGE_TYPE renderer
|
|
34
34
|
debug/collectors.ts: Diagnostic data collectors for /debug command, collectSystemInfo/collectModelInfo/collectSessionInfo/collectConfigInfo/collectGitInfo/collectAgentState, sanitizeForLLM, formatDiagnosticData
|
|
35
35
|
plan/index.ts: Plan Mode extension entry - registers /plan command, EnterPlanMode/ExitPlanMode tools, permission gating, workflow prompt injection
|
|
36
36
|
plan/types.ts: PlanModeState, PlanModeAttachment types, PlanModeConfig, PlanApprovalRequest/Response
|
|
@@ -42,14 +42,14 @@ plan/exit-plan-mode-tool.ts: createExitPlanModeTool() - ExitPlanMode tool with p
|
|
|
42
42
|
plan/plan-agents.ts: Explore/Plan subagent definitions with read-only tools for plan mode workflow
|
|
43
43
|
plan/plan-validation.ts: validatePlan() - validates plan has required sections (Context, Approach, Files, Verification)
|
|
44
44
|
plan/teammate-approval.ts: isInTeammateContext(), submitPlanToLeader(), formatPlanSubmittedMessage() - teammate plan approval integration
|
|
45
|
-
sal/index.ts: SAL extension entry, enabled by default, registers --nosal/--sal-rebuild-terrain flags, /sal:coverage /sal:status /sal:setup commands, before_agent_start/tool_execution_start/agent_end hooks; /sal:setup writes ~/.memory-experiments/credentials.json with adapter inference (insforge/jsonl/noop); publishes structuralAnchor via core/runtime/turn-context (no SAL-specific globals); emits run_start/turn_anchor/memory_recalls/run_end eval events through pluggable EvalSink; reads memoryRecallSnapshot from turn-context bus in agent_end; runtime no-op when --nosal is set
|
|
46
|
-
sal/terrain.ts: TerrainSnapshot/TerrainNode/TerrainEdge model, buildTerrainIndex()
|
|
45
|
+
sal/index.ts: SAL extension entry, enabled by default, registers --nosal/--sal-rebuild-terrain flags, /sal:coverage /sal:status /sal:setup commands, before_agent_start/tool_execution_start/tool_execution_end/agent_end hooks; /sal:setup writes ~/.memory-experiments/credentials.json with adapter inference (insforge/jsonl/noop); publishes structuralAnchor via core/runtime/turn-context (no SAL-specific globals); emits run_start/turn_anchor/memory_recalls/tool_trace/run_end eval events through pluggable EvalSink; reads memoryRecallSnapshot from turn-context bus in agent_end; runtime no-op when --nosal is set; auto-injects pencil_version from build-meta.json into run_start; emergency flush on beforeExit/SIGHUP/SIGTERM; stale run cleanup is opt-in via NANOPENCIL_EVAL_CLEANUP_STALE_RUNS / credentials cleanup_stale_runs; tool_trace is a bounded per-turn summary and includes no-tool turns
|
|
46
|
+
sal/terrain.ts: TerrainSnapshot/TerrainNode/TerrainEdge model, async buildTerrainIndex()/isSnapshotStale() (fs/promises + periodic yields so TUI can flush under block terminals like Warp), checkDipCoverage(), moduleIdForPath(), parses P2 CLAUDE.md and P3 file headers
|
|
47
47
|
sal/anchors.ts: StructuralAnchor/AnchorResolution model, locateTask(), locateAction(), evidence-driven scoring with tunable SalWeights, CJK bigram tokenization
|
|
48
48
|
sal/weights.ts: SalWeights interface, SAL_DEFAULT_WEIGHTS, loadSalWeights() reads sal-config.json from workspace or .memory-experiments/sal/
|
|
49
49
|
sal/eval/index.ts: createEvalSink() factory + barrel re-exports; adapter selection via options.adapter or endpoint scheme inference (http(s)→insforge, file://|/|./|../→jsonl, missing→noop); ONLY entry point SAL imports from
|
|
50
50
|
sal/eval/types.ts: EvalSink interface, EvalEventEnvelope/EvalEventType (run_start/run_end/turn_anchor/memory_recalls), EvalAdapterId ("insforge"|"jsonl"|"noop"), CreateEvalSinkOptions, createEvalEvent factory; zero-dependency type surface
|
|
51
51
|
sal/eval/noop-sink.ts: noopSink — silent EvalSink used when eval disabled or no adapter configured
|
|
52
|
-
sal/eval/insforge-sink.ts: InsForgeEvalSink — PostgREST adapter, routes run_start→eval_runs INSERT (merge-duplicates), turn_anchor→eval_turns + eval_sal_anchors×2, memory_recalls→eval_memory_recalls batch INSERT, run_end→eval_runs PATCH; allowSelfSigned TLS option, batching with default 2000ms interval
|
|
52
|
+
sal/eval/insforge-sink.ts: InsForgeEvalSink — PostgREST adapter, routes run_start→eval_runs INSERT (merge-duplicates), turn_anchor→eval_turns + eval_sal_anchors×2, tool_trace→eval_tool_traces bounded per-turn summaries (including no-tool turns and truncation counters), memory_recalls→eval_memory_recalls batch INSERT, run_end→eval_runs PATCH; allowSelfSigned TLS option, batching with default 2000ms interval
|
|
53
53
|
sal/eval/jsonl-sink.ts: JsonlEvalSink — append-only filesystem adapter, one JSON object per line, accepts file:// URLs or plain paths, auto-creates parent dir, batched writes
|
|
54
54
|
sal/README.md: SAL extension usage, sidecar output layout, weights override, pluggability contract
|
|
55
55
|
team/index.ts: AgentTeam extension entry, /team:/team:spawn/:send/:status/:stop/:terminate/:approve/:mode commands, TEAM_MESSAGE_TYPE renderer
|
|
@@ -64,4 +64,4 @@ team/TESTING.md: Manual & smoke-test guide for Phase B AgentTeam
|
|
|
64
64
|
|
|
65
65
|
Rule: Members complete, one item per line, parent links valid, precise terms first
|
|
66
66
|
|
|
67
|
-
[COVENANT]: Update this file header on changes and verify against parent CLAUDE.md
|
|
67
|
+
[COVENANT]: Update this file header on changes and verify against parent CLAUDE.md
|
|
@@ -82,4 +82,15 @@ export declare function collectGitInfo(cwd: string): Promise<CollectorResult<Git
|
|
|
82
82
|
export declare function collectAgentState(ctx: ExtensionContext): Promise<CollectorResult<AgentState>>;
|
|
83
83
|
export declare function sanitizeForLLM(data: DiagnosticData): DiagnosticData;
|
|
84
84
|
export declare function formatDiagnosticData(data: DiagnosticData): string;
|
|
85
|
+
export interface PreferencesInfo {
|
|
86
|
+
locale: string;
|
|
87
|
+
localeSource: "memory" | "settings" | "system";
|
|
88
|
+
memoryDir: string;
|
|
89
|
+
languagePreference: {
|
|
90
|
+
found: boolean;
|
|
91
|
+
name?: string;
|
|
92
|
+
summary?: string;
|
|
93
|
+
}[];
|
|
94
|
+
}
|
|
95
|
+
export declare function collectPreferencesInfo(ctx: ExtensionContext): Promise<CollectorResult<PreferencesInfo>>;
|
|
85
96
|
export {};
|
|
@@ -246,3 +246,70 @@ export function formatDiagnosticData(data) {
|
|
|
246
246
|
}
|
|
247
247
|
return parts.join("\n\n");
|
|
248
248
|
}
|
|
249
|
+
export async function collectPreferencesInfo(ctx) {
|
|
250
|
+
try {
|
|
251
|
+
const os = await import("node:os");
|
|
252
|
+
const fs = await import("node:fs");
|
|
253
|
+
const path = await import("node:path");
|
|
254
|
+
// Check memory directory for language preferences
|
|
255
|
+
const memoryDir = process.env.NANOMEM_MEMORY_DIR || path.join(os.homedir(), ".nanopencil", "agent", "memory");
|
|
256
|
+
let locale = "en";
|
|
257
|
+
let localeSource = "system";
|
|
258
|
+
const languagePreference = [];
|
|
259
|
+
// Try to read from preferences.json
|
|
260
|
+
const prefsPath = path.join(memoryDir, "preferences.json");
|
|
261
|
+
if (fs.existsSync(prefsPath)) {
|
|
262
|
+
try {
|
|
263
|
+
const prefs = JSON.parse(fs.readFileSync(prefsPath, "utf-8"));
|
|
264
|
+
// Find language-related preferences
|
|
265
|
+
const langPrefs = prefs.filter((p) => {
|
|
266
|
+
const text = (p.name || "") + (p.summary || "") + (p.detail || "");
|
|
267
|
+
return /中文|chinese|语言|locale|zh/i.test(text);
|
|
268
|
+
});
|
|
269
|
+
if (langPrefs.length > 0) {
|
|
270
|
+
locale = "zh";
|
|
271
|
+
localeSource = "memory";
|
|
272
|
+
for (const p of langPrefs.slice(0, 3)) {
|
|
273
|
+
languagePreference.push({
|
|
274
|
+
found: true,
|
|
275
|
+
name: p.name,
|
|
276
|
+
summary: (p.summary || "").slice(0, 80),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// Ignore read errors
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Check settings.json for locale
|
|
286
|
+
const settingsPath = path.join(os.homedir(), ".nanopencil", "agent", "settings.json");
|
|
287
|
+
if (localeSource === "system" && fs.existsSync(settingsPath)) {
|
|
288
|
+
try {
|
|
289
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
290
|
+
if (settings.locale) {
|
|
291
|
+
locale = settings.locale;
|
|
292
|
+
localeSource = "settings";
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// Ignore read errors
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
data: {
|
|
301
|
+
locale,
|
|
302
|
+
localeSource,
|
|
303
|
+
memoryDir,
|
|
304
|
+
languagePreference,
|
|
305
|
+
},
|
|
306
|
+
error: null,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
return {
|
|
311
|
+
data: null,
|
|
312
|
+
error: String(error),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* [WHO]: debugExtension -
|
|
2
|
+
* [WHO]: debugExtension - /debug command, before_agent_start hook injects diagnostic system prompt, agent_end cleanup, dispatched via sendUserMessage for streaming UX
|
|
3
3
|
* [FROM]: Depends on core/extensions/types, @pencil-agent/tui, ./collectors
|
|
4
4
|
* [TO]: Auto-loaded by builtin-extensions.ts as a default extension
|
|
5
|
-
* [HERE]: extensions/defaults/debug/index.ts - system diagnostics with three-layer analysis
|
|
5
|
+
* [HERE]: extensions/defaults/debug/index.ts - system diagnostics with three-layer analysis through full agent loop
|
|
6
6
|
*/
|
|
7
7
|
import type { ExtensionAPI } from "../../../core/extensions/types.js";
|
|
8
8
|
export default function debugExtension(api: ExtensionAPI): Promise<void>;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* [WHO]: debugExtension -
|
|
2
|
+
* [WHO]: debugExtension - /debug command, before_agent_start hook injects diagnostic system prompt, agent_end cleanup, dispatched via sendUserMessage for streaming UX
|
|
3
3
|
* [FROM]: Depends on core/extensions/types, @pencil-agent/tui, ./collectors
|
|
4
4
|
* [TO]: Auto-loaded by builtin-extensions.ts as a default extension
|
|
5
|
-
* [HERE]: extensions/defaults/debug/index.ts - system diagnostics with three-layer analysis
|
|
5
|
+
* [HERE]: extensions/defaults/debug/index.ts - system diagnostics with three-layer analysis through full agent loop
|
|
6
6
|
*/
|
|
7
7
|
import { Box, Container, Spacer, Text } from "@pencil-agent/tui";
|
|
8
8
|
import { collectSystemInfo, collectModelInfo, collectSessionInfo, collectConfigInfo, collectGitInfo, collectAgentState, sanitizeForLLM, formatDiagnosticData, } from "./collectors.js";
|
|
9
9
|
const DEBUG_MESSAGE_TYPE = "debug";
|
|
10
|
-
const
|
|
10
|
+
const DEBUG_PROMPT_PREFIX = "[DEBUG:";
|
|
11
|
+
const DEBUG_TAG = "[DEBUG]";
|
|
11
12
|
const DEBUG_SYSTEM_PROMPT = `You are a diagnostic analyst for nanoPencil (a terminal-native AI coding agent).
|
|
12
13
|
Analyze the provided system state and produce a structured three-layer diagnostic report.
|
|
13
14
|
|
|
@@ -37,7 +38,15 @@ Rules:
|
|
|
37
38
|
- If the user provided an issue description, focus analysis on that issue
|
|
38
39
|
- If no specific issue, perform a general health assessment
|
|
39
40
|
- Use concise language; prefer tables and bullet lists over prose
|
|
40
|
-
- If a diagnostic collection failed, treat that failure itself as a diagnostic signal
|
|
41
|
+
- If a diagnostic collection failed, treat that failure itself as a diagnostic signal
|
|
42
|
+
- Do NOT use any tools — this is a pure analysis task`;
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Pending diagnostic state (set by command handler, consumed by hooks)
|
|
45
|
+
// ============================================================================
|
|
46
|
+
let pendingDiagnosticPrompt;
|
|
47
|
+
function isDebugPrompt(text) {
|
|
48
|
+
return text.startsWith(DEBUG_PROMPT_PREFIX);
|
|
49
|
+
}
|
|
41
50
|
function parseDebugArgs(args) {
|
|
42
51
|
const trimmed = args.trim().toLowerCase();
|
|
43
52
|
if (trimmed === "env")
|
|
@@ -49,14 +58,12 @@ function parseDebugArgs(args) {
|
|
|
49
58
|
return { subcommand: "full", issueDescription: args.trim() || undefined };
|
|
50
59
|
}
|
|
51
60
|
// ============================================================================
|
|
52
|
-
// Full diagnostic flow
|
|
61
|
+
// Full diagnostic flow — collect then dispatch through agent loop
|
|
53
62
|
// ============================================================================
|
|
54
63
|
async function handleFullDiagnostic(args, ctx, api) {
|
|
55
64
|
const parsed = parseDebugArgs(args);
|
|
56
|
-
// Show status indicator
|
|
57
65
|
ctx.ui.setStatus("debug", "Collecting diagnostics...");
|
|
58
66
|
try {
|
|
59
|
-
// Collect all categories in parallel
|
|
60
67
|
const [system, model, session, config, git, agent] = await Promise.allSettled([
|
|
61
68
|
collectSystemInfo(),
|
|
62
69
|
collectModelInfo(ctx),
|
|
@@ -74,35 +81,17 @@ async function handleFullDiagnostic(args, ctx, api) {
|
|
|
74
81
|
agent: agent.status === "fulfilled" ? agent.value : { data: null, error: String(agent.reason) },
|
|
75
82
|
};
|
|
76
83
|
const data = sanitizeForLLM(raw);
|
|
77
|
-
|
|
84
|
+
ctx.ui.setStatus("debug", undefined);
|
|
78
85
|
const parts = [];
|
|
86
|
+
parts.push(`${DEBUG_TAG} Perform a three-layer diagnostic analysis.`);
|
|
79
87
|
if (parsed.issueDescription) {
|
|
80
|
-
parts.push(
|
|
88
|
+
parts.push(`\nUser-Reported Issue: ${parsed.issueDescription}`);
|
|
81
89
|
}
|
|
82
|
-
parts.push(
|
|
90
|
+
parts.push(`\nCollected Diagnostics:\n`);
|
|
83
91
|
parts.push(formatDiagnosticData(data));
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
ctx.completeSimple(DEBUG_SYSTEM_PROMPT, userMessage),
|
|
88
|
-
new Promise((resolve) => setTimeout(() => resolve(undefined), DEBUG_TIMEOUT_MS)),
|
|
89
|
-
]);
|
|
90
|
-
ctx.ui.setStatus("debug", undefined);
|
|
91
|
-
if (response) {
|
|
92
|
-
api.sendMessage({
|
|
93
|
-
customType: DEBUG_MESSAGE_TYPE,
|
|
94
|
-
content: response,
|
|
95
|
-
display: true,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
// LLM unavailable — show raw data as fallback
|
|
100
|
-
api.sendMessage({
|
|
101
|
-
customType: DEBUG_MESSAGE_TYPE,
|
|
102
|
-
content: `**LLM analysis unavailable** (timeout or no API key). Raw diagnostics:\n\n${formatDiagnosticData(data)}`,
|
|
103
|
-
display: true,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
92
|
+
const prompt = `${DEBUG_PROMPT_PREFIX}${Date.now()}]\n${parts.join("\n")}`;
|
|
93
|
+
pendingDiagnosticPrompt = prompt;
|
|
94
|
+
api.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
106
95
|
}
|
|
107
96
|
catch (error) {
|
|
108
97
|
ctx.ui.setStatus("debug", undefined);
|
|
@@ -111,7 +100,7 @@ async function handleFullDiagnostic(args, ctx, api) {
|
|
|
111
100
|
}
|
|
112
101
|
}
|
|
113
102
|
// ============================================================================
|
|
114
|
-
// Quick subcommand — show raw data without
|
|
103
|
+
// Quick subcommand — show raw data without agent loop
|
|
115
104
|
// ============================================================================
|
|
116
105
|
async function handleQuickSub(subcommand, ctx, api) {
|
|
117
106
|
let result;
|
|
@@ -166,7 +155,6 @@ async function handleDebugCommand(args, ctx, api) {
|
|
|
166
155
|
// Extension entry
|
|
167
156
|
// ============================================================================
|
|
168
157
|
export default async function debugExtension(api) {
|
|
169
|
-
// Register debug message renderer (same pattern as btw)
|
|
170
158
|
api.registerMessageRenderer(DEBUG_MESSAGE_TYPE, (message, _options, theme) => {
|
|
171
159
|
const text = typeof message.content === "string"
|
|
172
160
|
? message.content
|
|
@@ -181,9 +169,75 @@ export default async function debugExtension(api) {
|
|
|
181
169
|
container.addChild(box);
|
|
182
170
|
return container;
|
|
183
171
|
});
|
|
184
|
-
|
|
172
|
+
api.on("before_agent_start", (event) => {
|
|
173
|
+
if (!isDebugPrompt(event.prompt))
|
|
174
|
+
return;
|
|
175
|
+
return { appendSystemPrompt: DEBUG_SYSTEM_PROMPT };
|
|
176
|
+
});
|
|
177
|
+
api.on("agent_end", () => {
|
|
178
|
+
if (pendingDiagnosticPrompt) {
|
|
179
|
+
pendingDiagnosticPrompt = undefined;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
185
182
|
api.registerCommand("debug", {
|
|
186
|
-
description: "Run system diagnostics
|
|
183
|
+
description: "Run system diagnostics (/debug [env|session|model|preferences|<issue>])",
|
|
187
184
|
handler: (args, ctx) => handleDebugCommand(args, ctx, api),
|
|
188
185
|
});
|
|
186
|
+
// Register /set-locale command
|
|
187
|
+
api.registerCommand("set-locale", {
|
|
188
|
+
description: "Set language preference (/set-locale zh|en)",
|
|
189
|
+
handler: async (args, ctx) => {
|
|
190
|
+
const trimmed = args.trim().toLowerCase();
|
|
191
|
+
if (trimmed !== "zh" && trimmed !== "en") {
|
|
192
|
+
ctx.ui.notify("Usage: /set-locale zh or /set-locale en", "info");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// Get memory directory
|
|
196
|
+
const os = await import("node:os");
|
|
197
|
+
const fs = await import("node:fs");
|
|
198
|
+
const path = await import("node:path");
|
|
199
|
+
const memoryDir = process.env.NANOMEM_MEMORY_DIR || path.join(os.homedir(), ".nanopencil", "agent", "memory");
|
|
200
|
+
const prefsPath = path.join(memoryDir, "preferences.json");
|
|
201
|
+
try {
|
|
202
|
+
let prefs = [];
|
|
203
|
+
if (fs.existsSync(prefsPath)) {
|
|
204
|
+
prefs = JSON.parse(fs.readFileSync(prefsPath, "utf-8"));
|
|
205
|
+
}
|
|
206
|
+
// Check if language preference already exists
|
|
207
|
+
const existingIndex = prefs.findIndex((p) => {
|
|
208
|
+
const name = p.name || "";
|
|
209
|
+
return name.includes("用户偏好") || name.includes("language preference") || name.includes("locale");
|
|
210
|
+
});
|
|
211
|
+
const newPref = {
|
|
212
|
+
id: `set-locale-${Date.now()}`,
|
|
213
|
+
type: "preference",
|
|
214
|
+
name: trimmed === "zh" ? "用户偏好中文" : "Language Preference (English)",
|
|
215
|
+
summary: trimmed === "zh" ? "用户希望我用中文回复" : "User prefers English",
|
|
216
|
+
detail: trimmed === "zh" ? "用户通过 /set-locale 命令明确设置语言为中文" : "User explicitly set language to English via /set-locale command",
|
|
217
|
+
content: trimmed === "zh" ? "用户希望用中文回复" : "User prefers English responses",
|
|
218
|
+
tags: ["locale", "language", trimmed === "zh" ? "中文" : "english"],
|
|
219
|
+
importance: 10,
|
|
220
|
+
strength: 1000,
|
|
221
|
+
created: new Date().toISOString(),
|
|
222
|
+
eventTime: new Date().toISOString(),
|
|
223
|
+
accessCount: 0,
|
|
224
|
+
retention: "core",
|
|
225
|
+
salience: 10,
|
|
226
|
+
stability: "stable",
|
|
227
|
+
relations: [],
|
|
228
|
+
};
|
|
229
|
+
if (existingIndex >= 0) {
|
|
230
|
+
prefs[existingIndex] = newPref;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
prefs.push(newPref);
|
|
234
|
+
}
|
|
235
|
+
fs.writeFileSync(prefsPath, JSON.stringify(prefs, null, 2));
|
|
236
|
+
ctx.ui.notify(`Locale set to ${trimmed === "zh" ? "中文" : "English"}. Restart or run /debug preferences to verify.`, "info");
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
ctx.ui.notify(`Failed to set locale: ${error}`, "error");
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
});
|
|
189
243
|
}
|
|
@@ -7,13 +7,60 @@
|
|
|
7
7
|
import type { ExtensionAPI } from "../../../core/extensions/types.js";
|
|
8
8
|
declare function getFallbackOpeningLines(locale?: "en" | "zh"): string[];
|
|
9
9
|
declare function getFallbackIdleLines(locale?: "en" | "zh"): string[];
|
|
10
|
+
type PresenceState = {
|
|
11
|
+
lastActivityAt: number;
|
|
12
|
+
idleReminderSent: boolean;
|
|
13
|
+
openingStartedAt?: number;
|
|
14
|
+
openingSent: boolean;
|
|
15
|
+
openingTimer?: ReturnType<typeof setTimeout>;
|
|
16
|
+
idleTimer?: ReturnType<typeof setInterval>;
|
|
17
|
+
unsubscribeInput?: () => void;
|
|
18
|
+
memEngine?: {
|
|
19
|
+
getAllEntries(): Promise<{
|
|
20
|
+
knowledge: Array<{
|
|
21
|
+
type?: string;
|
|
22
|
+
tags: string[];
|
|
23
|
+
name?: string;
|
|
24
|
+
summary?: string;
|
|
25
|
+
detail?: string;
|
|
26
|
+
content?: string;
|
|
27
|
+
}>;
|
|
28
|
+
lessons: Array<{
|
|
29
|
+
type?: string;
|
|
30
|
+
tags: string[];
|
|
31
|
+
name?: string;
|
|
32
|
+
summary?: string;
|
|
33
|
+
detail?: string;
|
|
34
|
+
content?: string;
|
|
35
|
+
importance?: number;
|
|
36
|
+
}>;
|
|
37
|
+
}>;
|
|
38
|
+
getAllEpisodes(): Promise<Array<{
|
|
39
|
+
date?: string;
|
|
40
|
+
consolidated?: boolean;
|
|
41
|
+
endedAt?: string;
|
|
42
|
+
startedAt?: string;
|
|
43
|
+
summary?: string;
|
|
44
|
+
userGoal?: string;
|
|
45
|
+
}>>;
|
|
46
|
+
searchEntries(query: string): Promise<Array<{
|
|
47
|
+
type?: string;
|
|
48
|
+
tags: string[];
|
|
49
|
+
}>>;
|
|
50
|
+
};
|
|
51
|
+
recentPresenceLines: string[];
|
|
52
|
+
lastPresenceAt?: number;
|
|
53
|
+
idleGenerating?: boolean;
|
|
54
|
+
};
|
|
10
55
|
declare function resolveBundledPackageEntry(packageName: "mem-core" | "soul-core"): string | undefined;
|
|
11
56
|
declare function importRuntimeModule<T>(moduleNames: string[], bundledPackageName?: "mem-core" | "soul-core"): Promise<T | undefined>;
|
|
57
|
+
declare function detectLanguageFromMemory(state: PresenceState): Promise<"en" | "zh" | undefined>;
|
|
12
58
|
export default function presenceExtension(api: ExtensionAPI): Promise<void>;
|
|
13
59
|
export declare const __testUtils: {
|
|
14
60
|
getFallbackOpeningLines: typeof getFallbackOpeningLines;
|
|
15
61
|
getFallbackIdleLines: typeof getFallbackIdleLines;
|
|
16
62
|
resolveBundledPackageEntry: typeof resolveBundledPackageEntry;
|
|
17
63
|
importRuntimeModule: typeof importRuntimeModule;
|
|
64
|
+
detectLanguageFromMemory: typeof detectLanguageFromMemory;
|
|
18
65
|
};
|
|
19
66
|
export {};
|
|
@@ -153,10 +153,6 @@ function clearTimers(state) {
|
|
|
153
153
|
state.unsubscribeInput?.();
|
|
154
154
|
state.unsubscribeInput = undefined;
|
|
155
155
|
}
|
|
156
|
-
function getMemoryDir() {
|
|
157
|
-
// Use the same memory directory as the main app
|
|
158
|
-
return process.env.NANOMEM_MEMORY_DIR || join(homedir(), ".nanomem", "memory");
|
|
159
|
-
}
|
|
160
156
|
async function initMemEngine(state) {
|
|
161
157
|
if (state.memEngine)
|
|
162
158
|
return;
|
|
@@ -176,6 +172,18 @@ async function initMemEngine(state) {
|
|
|
176
172
|
state.memEngine = undefined;
|
|
177
173
|
}
|
|
178
174
|
}
|
|
175
|
+
function getMemoryDir() {
|
|
176
|
+
// Use the same memory directory as the main app
|
|
177
|
+
// Priority: env var > nanopencil default > legacy nanomem path
|
|
178
|
+
if (process.env.NANOMEM_MEMORY_DIR)
|
|
179
|
+
return process.env.NANOMEM_MEMORY_DIR;
|
|
180
|
+
// Check if nanopencil's memory directory exists
|
|
181
|
+
const nanopencilMemory = join(homedir(), ".nanopencil", "agent", "memory");
|
|
182
|
+
if (existsSync(nanopencilMemory))
|
|
183
|
+
return nanopencilMemory;
|
|
184
|
+
// Fallback to legacy path
|
|
185
|
+
return join(homedir(), ".nanomem", "memory");
|
|
186
|
+
}
|
|
179
187
|
function getProject() {
|
|
180
188
|
const parts = process.cwd().split("/").filter(Boolean);
|
|
181
189
|
return parts.length >= 2
|
|
@@ -203,21 +211,32 @@ async function detectLanguageFromMemory(state) {
|
|
|
203
211
|
}
|
|
204
212
|
}
|
|
205
213
|
catch { /* ignore */ }
|
|
214
|
+
let zhScore = 0;
|
|
215
|
+
let enScore = 0;
|
|
216
|
+
const zhTerms = "(中文|chinese|zh-hans|mandarin|普通话)";
|
|
217
|
+
const enTerms = "(英文|english|en-us)";
|
|
218
|
+
const negPrefix = "(?:don't|do not|no|not|不用|不要|别|不想用)";
|
|
219
|
+
const useWords = "(?:\\s+use|\\s+using|\\s+说|\\s+讲|\\s+用)?";
|
|
220
|
+
const zhNegative = new RegExp(`${negPrefix}${useWords}\\s*${zhTerms}`);
|
|
221
|
+
const enNegative = new RegExp(`${negPrefix}${useWords}\\s*${enTerms}`);
|
|
222
|
+
const zhPositive = new RegExp(zhTerms);
|
|
223
|
+
const enPositive = new RegExp(enTerms);
|
|
206
224
|
// Check preference content for language indicators
|
|
207
225
|
for (const pref of preferences) {
|
|
208
226
|
const text = `${pref.name || ""} ${pref.summary || ""} ${pref.detail || ""} ${pref.content || ""}`.toLowerCase();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
227
|
+
const hasZh = zhPositive.test(text);
|
|
228
|
+
const hasEn = enPositive.test(text);
|
|
229
|
+
const noZh = zhNegative.test(text);
|
|
230
|
+
const noEn = enNegative.test(text);
|
|
231
|
+
if (hasZh && !noZh)
|
|
232
|
+
zhScore += 2;
|
|
233
|
+
if (hasEn && !noEn)
|
|
234
|
+
enScore += 2;
|
|
235
|
+
// Cross-language hints: "don't use Chinese" slightly supports English, and vice versa.
|
|
236
|
+
if (noZh)
|
|
237
|
+
enScore += 1;
|
|
238
|
+
if (noEn)
|
|
239
|
+
zhScore += 1;
|
|
221
240
|
}
|
|
222
241
|
// Check recent episodes for language patterns
|
|
223
242
|
const episodes = await state.memEngine.getAllEpisodes();
|
|
@@ -233,8 +252,12 @@ async function detectLanguageFromMemory(state) {
|
|
|
233
252
|
englishContent++;
|
|
234
253
|
}
|
|
235
254
|
if (chineseContent > englishContent)
|
|
236
|
-
|
|
255
|
+
zhScore += 1;
|
|
237
256
|
if (englishContent > chineseContent && englishContent > 2)
|
|
257
|
+
enScore += 1;
|
|
258
|
+
if (zhScore > enScore && zhScore > 0)
|
|
259
|
+
return "zh";
|
|
260
|
+
if (enScore > zhScore && enScore > 0)
|
|
238
261
|
return "en";
|
|
239
262
|
return undefined;
|
|
240
263
|
}
|
|
@@ -437,9 +460,10 @@ function getLastUserMessage(ctx) {
|
|
|
437
460
|
const entry = entries[i];
|
|
438
461
|
if (entry.type !== "message")
|
|
439
462
|
continue;
|
|
440
|
-
|
|
463
|
+
const message = entry.message;
|
|
464
|
+
if (!message || message.role !== "user")
|
|
441
465
|
continue;
|
|
442
|
-
const c =
|
|
466
|
+
const c = message.content;
|
|
443
467
|
if (typeof c === "string")
|
|
444
468
|
return c;
|
|
445
469
|
if (Array.isArray(c)) {
|
|
@@ -705,4 +729,5 @@ export const __testUtils = {
|
|
|
705
729
|
getFallbackIdleLines,
|
|
706
730
|
resolveBundledPackageEntry,
|
|
707
731
|
importRuntimeModule,
|
|
732
|
+
detectLanguageFromMemory,
|
|
708
733
|
};
|
|
@@ -20,6 +20,10 @@ pencil --nosal -p "your prompt"
|
|
|
20
20
|
|
|
21
21
|
When `--nosal` is set, all hooks return early and zero work is performed.
|
|
22
22
|
|
|
23
|
+
## Terminal compatibility (Warp, block UIs)
|
|
24
|
+
|
|
25
|
+
SAL builds a **terrain snapshot** of the workspace (walk + read DIP headers). That work is **asynchronous and periodically yields to the event loop** so the TUI can flush user input and status lines to the terminal while indexing runs. If you still see UI glitches in a specific terminal, use `--nosal` to confirm whether SAL is involved, then file an issue with `TERM_PROGRAM`, Warp version, and repro steps.
|
|
26
|
+
|
|
23
27
|
## Slash commands
|
|
24
28
|
|
|
25
29
|
| Command | Purpose |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* [WHO]: Provides InsForgeEvalSink (PostgREST-backed adapter)
|
|
3
3
|
* [FROM]: Depends on node:https, node:http, node:url; ./types.js for EvalSink/EvalEventEnvelope/CreateEvalSinkOptions
|
|
4
4
|
* [TO]: Constructed by eval/index.ts factory when adapter resolves to "insforge"
|
|
5
|
-
* [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - InsForge-specific routing: run_start→eval_runs INSERT (merge-duplicates), turn_anchor→eval_turns + eval_sal_anchors×2, run_end→eval_runs PATCH
|
|
5
|
+
* [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - InsForge-specific routing: run_start→eval_runs INSERT (merge-duplicates, includes pencil_version), turn_anchor→eval_turns + eval_sal_anchors×2, tool_trace→eval_tool_traces, memory_recalls→eval_memory_recalls, run_end→eval_runs PATCH
|
|
6
6
|
*
|
|
7
7
|
* Pluggable: nothing in this file may be imported from outside the eval/ directory.
|
|
8
8
|
* To add a new backend, write a sibling file with the same EvalSink interface.
|
|
@@ -29,6 +29,7 @@ export declare class InsForgeEvalSink implements EvalSink {
|
|
|
29
29
|
private handleTurnAnchor;
|
|
30
30
|
private handleRunEnd;
|
|
31
31
|
private handleMemoryRecalls;
|
|
32
|
+
private handleToolTrace;
|
|
32
33
|
private postJson;
|
|
33
34
|
private patchJson;
|
|
34
35
|
private httpJson;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* [WHO]: Provides InsForgeEvalSink (PostgREST-backed adapter)
|
|
3
3
|
* [FROM]: Depends on node:https, node:http, node:url; ./types.js for EvalSink/EvalEventEnvelope/CreateEvalSinkOptions
|
|
4
4
|
* [TO]: Constructed by eval/index.ts factory when adapter resolves to "insforge"
|
|
5
|
-
* [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - InsForge-specific routing: run_start→eval_runs INSERT (merge-duplicates), turn_anchor→eval_turns + eval_sal_anchors×2, run_end→eval_runs PATCH
|
|
5
|
+
* [HERE]: extensions/defaults/sal/eval/insforge-sink.ts - InsForge-specific routing: run_start→eval_runs INSERT (merge-duplicates, includes pencil_version), turn_anchor→eval_turns + eval_sal_anchors×2, tool_trace→eval_tool_traces, memory_recalls→eval_memory_recalls, run_end→eval_runs PATCH
|
|
6
6
|
*
|
|
7
7
|
* Pluggable: nothing in this file may be imported from outside the eval/ directory.
|
|
8
8
|
* To add a new backend, write a sibling file with the same EvalSink interface.
|
|
@@ -103,6 +103,9 @@ export class InsForgeEvalSink {
|
|
|
103
103
|
case "memory_recalls":
|
|
104
104
|
await this.handleMemoryRecalls(event);
|
|
105
105
|
break;
|
|
106
|
+
case "tool_trace":
|
|
107
|
+
await this.handleToolTrace(event);
|
|
108
|
+
break;
|
|
106
109
|
case "run_end":
|
|
107
110
|
await this.handleRunEnd(event);
|
|
108
111
|
break;
|
|
@@ -123,6 +126,7 @@ export class InsForgeEvalSink {
|
|
|
123
126
|
task_file: strOrNull(p.task_file),
|
|
124
127
|
model: strOrNull(p.model),
|
|
125
128
|
thinking: p.thinking === true,
|
|
129
|
+
pencil_version: strOrNull(p.pencil_version),
|
|
126
130
|
commit_hash: strOrNull(p.commit, "unknown"),
|
|
127
131
|
branch_name: strOrNull(p.branch, "unknown"),
|
|
128
132
|
workspace_root: strOrNull(p.workspace_root),
|
|
@@ -213,6 +217,31 @@ export class InsForgeEvalSink {
|
|
|
213
217
|
}));
|
|
214
218
|
await this.postJson(`${this.base}/api/database/records/eval_memory_recalls`, rows, { prefer: "resolution=ignore-duplicates" });
|
|
215
219
|
}
|
|
220
|
+
// INSERT into eval_tool_traces — one row per turn with tool usage summary
|
|
221
|
+
// InsForge columns are all TEXT; JSONB fields must be serialized to strings.
|
|
222
|
+
async handleToolTrace(ev) {
|
|
223
|
+
const p = ev.payload;
|
|
224
|
+
const taskSignals = p.task_signals;
|
|
225
|
+
await this.postJson(`${this.base}/api/database/records/eval_tool_traces`, [{
|
|
226
|
+
run_id: ev.run_id,
|
|
227
|
+
turn_id: String(p.turn_id ?? 0),
|
|
228
|
+
event_id: ev.event_id,
|
|
229
|
+
tool_calls: p.tool_calls ? JSON.stringify(p.tool_calls) : null,
|
|
230
|
+
tool_sequence: p.tool_sequence ? JSON.stringify(p.tool_sequence) : null,
|
|
231
|
+
intent: strOrNull(taskSignals?.intent),
|
|
232
|
+
prompt_length: String(taskSignals?.prompt_length ?? 0),
|
|
233
|
+
has_error_trace: String(taskSignals?.has_error_trace === true),
|
|
234
|
+
has_file_reference: String(taskSignals?.has_file_reference === true),
|
|
235
|
+
has_tool_usage: String(p.has_tool_usage === true),
|
|
236
|
+
total_tool_calls: String(p.total_tool_calls ?? 0),
|
|
237
|
+
total_errors: String(p.total_errors ?? 0),
|
|
238
|
+
completed_tool_calls: String(p.completed_tool_calls ?? 0),
|
|
239
|
+
truncated_tool_calls: String(p.truncated_tool_calls ?? 0),
|
|
240
|
+
truncated_tool_summary: String(p.truncated_tool_summary ?? 0),
|
|
241
|
+
duration_ms: String(p.duration_ms ?? 0),
|
|
242
|
+
recorded_at: ev.ts,
|
|
243
|
+
}], { prefer: "resolution=ignore-duplicates" });
|
|
244
|
+
}
|
|
216
245
|
// ------------------------------------------------------------------
|
|
217
246
|
// HTTP helpers
|
|
218
247
|
// ------------------------------------------------------------------
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* [HERE]: extensions/defaults/sal/eval/types.ts - transport-agnostic event types and the EvalSink contract; concrete adapters live in sibling files
|
|
6
6
|
*/
|
|
7
7
|
export type EvalVariant = "sal" | "control" | "baseline";
|
|
8
|
-
export type EvalEventType = "run_start" | "run_end" | "turn_anchor" | "memory_recalls";
|
|
8
|
+
export type EvalEventType = "run_start" | "run_end" | "turn_anchor" | "memory_recalls" | "tool_trace";
|
|
9
9
|
/** Wire format for eval events. Adapter implementations decide how to materialize. */
|
|
10
10
|
export interface EvalEventEnvelope {
|
|
11
11
|
run_id: string;
|