@pencil-agent/nano-pencil 1.14.2 → 1.14.4

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.
Files changed (104) hide show
  1. package/dist/build-meta.json +3 -3
  2. package/dist/builtin-extensions.d.ts +4 -0
  3. package/dist/builtin-extensions.js +16 -16
  4. package/dist/core/config/settings-manager.d.ts +8 -1
  5. package/dist/core/config/settings-manager.js +9 -0
  6. package/dist/core/extensions/runner.d.ts +70 -4
  7. package/dist/core/extensions/runner.js +188 -8
  8. package/dist/core/extensions/types.d.ts +8 -1
  9. package/dist/core/i18n/slash-commands.d.ts +12 -0
  10. package/dist/core/i18n/slash-commands.js +16 -4
  11. package/dist/core/i18n/slash-commands.zh.d.ts +12 -0
  12. package/dist/core/i18n/slash-commands.zh.js +16 -4
  13. package/dist/core/runtime/agent-session.d.ts +5 -0
  14. package/dist/core/runtime/agent-session.js +85 -27
  15. package/dist/core/runtime/extension-core-bindings.d.ts +3 -3
  16. package/dist/core/runtime/extension-core-bindings.js +73 -20
  17. package/dist/core/runtime/retry-coordinator.d.ts +10 -1
  18. package/dist/core/runtime/retry-coordinator.js +20 -4
  19. package/dist/core/runtime/sdk.js +2 -1
  20. package/dist/core/runtime/slash-command-catalog.d.ts +2 -1
  21. package/dist/core/runtime/slash-command-catalog.js +9 -1
  22. package/dist/core/slash-commands.d.ts +12 -1
  23. package/dist/core/slash-commands.js +81 -32
  24. package/dist/core/telemetry/batching-dispatcher.d.ts +41 -0
  25. package/dist/core/telemetry/batching-dispatcher.js +89 -0
  26. package/dist/core/telemetry/build-meta.d.ts +12 -0
  27. package/dist/core/telemetry/build-meta.js +57 -0
  28. package/dist/core/telemetry/caller-context.d.ts +32 -0
  29. package/dist/core/telemetry/caller-context.js +19 -0
  30. package/dist/core/telemetry/credentials.d.ts +27 -0
  31. package/dist/core/telemetry/credentials.js +87 -0
  32. package/dist/core/telemetry/ext-events.d.ts +89 -0
  33. package/dist/core/telemetry/ext-events.js +189 -0
  34. package/dist/core/telemetry/index.d.ts +13 -0
  35. package/dist/core/telemetry/index.js +6 -0
  36. package/dist/core/telemetry/insforge-base.d.ts +37 -0
  37. package/dist/core/telemetry/insforge-base.js +160 -0
  38. package/dist/core/telemetry/types.d.ts +33 -0
  39. package/dist/core/telemetry/types.js +7 -0
  40. package/dist/extensions/defaults/browser/index.js +14 -6
  41. package/dist/extensions/defaults/btw/index.js +2 -2
  42. package/dist/extensions/defaults/debug/index.js +38 -3
  43. package/dist/extensions/defaults/diagnostics/index.js +12 -0
  44. package/dist/extensions/defaults/grub/grub-parser.d.ts +15 -1
  45. package/dist/extensions/defaults/grub/grub-parser.js +31 -1
  46. package/dist/extensions/defaults/grub/index.d.ts +1 -1
  47. package/dist/extensions/defaults/grub/index.js +4 -3
  48. package/dist/extensions/defaults/interview/index.js +2 -2
  49. package/dist/extensions/defaults/link-world/index.js +14 -6
  50. package/dist/extensions/defaults/loop/cron/cron-scheduler.js +19 -0
  51. package/dist/extensions/defaults/loop/index.js +35 -0
  52. package/dist/extensions/defaults/mcp/index.js +18 -0
  53. package/dist/extensions/defaults/plan/index.js +29 -11
  54. package/dist/extensions/defaults/presence/index.d.ts +12 -2
  55. package/dist/extensions/defaults/presence/index.js +77 -23
  56. package/dist/extensions/defaults/presence/presence-memory.d.ts +2 -1
  57. package/dist/extensions/defaults/presence/presence-memory.js +37 -1
  58. package/dist/extensions/defaults/recap/index.js +12 -0
  59. package/dist/extensions/defaults/sal/eval/insforge-sink.d.ts +6 -17
  60. package/dist/extensions/defaults/sal/eval/insforge-sink.js +40 -183
  61. package/dist/extensions/defaults/sal/index.js +31 -8
  62. package/dist/extensions/defaults/sal/sal-config.d.ts +5 -0
  63. package/dist/extensions/defaults/sal/sal-config.js +15 -82
  64. package/dist/extensions/defaults/security-audit/index.js +141 -83
  65. package/dist/extensions/defaults/subagent/index.js +29 -5
  66. package/dist/extensions/defaults/team/index.js +10 -9
  67. package/dist/extensions/defaults/team/team-parser.d.ts +18 -0
  68. package/dist/extensions/defaults/team/team-parser.js +91 -3
  69. package/dist/extensions/defaults/team/team-ui.js +4 -14
  70. package/dist/extensions/defaults/token-save/index.js +14 -1
  71. package/dist/extensions/optional/export-html/index.js +19 -5
  72. package/dist/extensions/optional/simplify/index.js +11 -5
  73. package/dist/modes/interactive/interactive-mode.d.ts +2 -1
  74. package/dist/modes/interactive/interactive-mode.js +68 -19
  75. package/dist/modes/interactive/slash-command-arguments.d.ts +16 -0
  76. package/dist/modes/interactive/slash-command-arguments.js +97 -0
  77. package/dist/modes/rpc/rpc-mode.d.ts +3 -0
  78. package/dist/modes/rpc/rpc-mode.js +40 -31
  79. package/dist/modes/rpc/rpc-types.d.ts +3 -0
  80. package/dist/node_modules/@pencil-agent/agent-core/agent.d.ts +15 -1
  81. package/dist/node_modules/@pencil-agent/agent-core/agent.js +13 -1
  82. package/dist/node_modules/@pencil-agent/agent-core/index.d.ts +2 -1
  83. package/dist/node_modules/@pencil-agent/agent-core/index.js +2 -1
  84. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.d.ts +1 -1
  85. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-agent-loop.js +293 -20
  86. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.d.ts +33 -0
  87. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-streaming-tool-executor.js +189 -0
  88. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.d.ts +9 -0
  89. package/dist/node_modules/@pencil-agent/agent-core/structured-adaptive-tool-orchestration.js +30 -5
  90. package/dist/node_modules/@pencil-agent/agent-core/types.d.ts +90 -3
  91. package/dist/node_modules/@pencil-agent/agent-core/types.js +1 -1
  92. package/dist/node_modules/@pencil-agent/ai/models.generated.d.ts +0 -17
  93. package/dist/node_modules/@pencil-agent/ai/models.generated.js +29 -46
  94. package/dist/node_modules/@pencil-agent/tui/autocomplete.d.ts +8 -1
  95. package/dist/node_modules/@pencil-agent/tui/autocomplete.js +18 -2
  96. package/dist/node_modules/@pencil-agent/tui/index.d.ts +1 -1
  97. package/dist/node_modules/@pencil-agent/tui/tui.d.ts +8 -3
  98. package/dist/node_modules/@pencil-agent/tui/tui.js +68 -33
  99. package/dist/packages/mem-core/extension.js +154 -83
  100. package/dist/packages/mem-core/full-insights-sections.d.ts +48 -0
  101. package/dist/packages/mem-core/full-insights-sections.js +231 -0
  102. package/dist/packages/mem-core/full-insights.d.ts +1 -1
  103. package/dist/packages/mem-core/full-insights.js +102 -42
  104. package/package.json +2 -2
@@ -0,0 +1,189 @@
1
+ /**
2
+ * [WHO]: Provides ExtensionTelemetrySink interface, CommandEventInput, classifyArgsSignature(), createExtensionTelemetrySink() factory (insforge-backed or noop)
3
+ * [FROM]: Depends on ./insforge-base (InsforgeHttpClient), ./batching-dispatcher (BatchingDispatcher), ./credentials (loadInsforgeCredentials), ./build-meta (loadBuildMeta), ./types (DiagnosticHandler)
4
+ * [TO]: Consumed by core/extensions/runner.ts (writes one row per /command invocation); future P2/P3 commits add LLM-call + hook-event sinks alongside this one
5
+ * [HERE]: core/telemetry/ext-events.ts - P1 writer for ext_command_events table; one row per extension command invocation, batched + fire-and-forget, no-op when no insforge credentials configured
6
+ */
7
+ import { BatchingDispatcher } from "./batching-dispatcher.js";
8
+ import { loadBuildMeta } from "./build-meta.js";
9
+ import { loadInsforgeCredentials } from "./credentials.js";
10
+ import { InsforgeHttpClient } from "./insforge-base.js";
11
+ const BUILD_META = loadBuildMeta();
12
+ const TELEMETRY_SOURCE = "ext.telemetry";
13
+ const EXT_COMMAND_EVENTS_TABLE = "ext_command_events";
14
+ const EXT_LLM_CALLS_TABLE = "ext_llm_calls";
15
+ const EXT_HOOK_EVENTS_TABLE = "ext_hook_events";
16
+ /**
17
+ * Sample rates per hook name. Tool-related hooks fire many times per turn
18
+ * (one pair per tool call), so we record only 10% to avoid drowning the
19
+ * table; the sample_rate column lets dashboards extrapolate counts with
20
+ * `count(*) * (1.0 / avg(sample_rate))`. Everything else is rare enough
21
+ * to record in full.
22
+ */
23
+ export const HOOK_SAMPLE_RATES = Object.freeze({
24
+ tool_call: 0.1,
25
+ tool_result: 0.1,
26
+ tool_execution_start: 0.1,
27
+ tool_execution_end: 0.1,
28
+ });
29
+ class NoopExtensionTelemetrySink {
30
+ writeCommandEvent() {
31
+ // no-op
32
+ }
33
+ writeLlmCallEvent() {
34
+ // no-op
35
+ }
36
+ writeHookEvent() {
37
+ // no-op
38
+ }
39
+ async close() {
40
+ // no-op
41
+ }
42
+ }
43
+ class InsforgeExtensionTelemetrySink {
44
+ http;
45
+ dispatcher;
46
+ constructor(opts) {
47
+ this.http = new InsforgeHttpClient({
48
+ endpoint: opts.endpoint,
49
+ apiKey: opts.apiKey,
50
+ anonKey: opts.anonKey,
51
+ apiKeyHeader: opts.apiKeyHeader,
52
+ extraHeaders: opts.extraHeaders,
53
+ allowSelfSigned: opts.allowSelfSigned,
54
+ source: TELEMETRY_SOURCE,
55
+ onDiagnostic: opts.onDiagnostic,
56
+ });
57
+ this.dispatcher = new BatchingDispatcher({
58
+ handler: (event) => this.postEvent(event),
59
+ intervalMs: opts.batchIntervalMs ?? 2000,
60
+ source: TELEMETRY_SOURCE,
61
+ onDiagnostic: opts.onDiagnostic,
62
+ });
63
+ }
64
+ writeCommandEvent(input) {
65
+ this.dispatcher.enqueue({ type: "command", payload: input });
66
+ }
67
+ writeLlmCallEvent(input) {
68
+ this.dispatcher.enqueue({ type: "llm_call", payload: input });
69
+ }
70
+ writeHookEvent(input) {
71
+ this.dispatcher.enqueue({ type: "hook", payload: input });
72
+ }
73
+ async close() {
74
+ await this.dispatcher.close();
75
+ }
76
+ async postEvent(event) {
77
+ if (event.type === "command") {
78
+ await this.postCommandEvent(event.payload);
79
+ }
80
+ else if (event.type === "llm_call") {
81
+ await this.postLlmCallEvent(event.payload);
82
+ }
83
+ else {
84
+ await this.postHookEvent(event.payload);
85
+ }
86
+ }
87
+ async postCommandEvent(input) {
88
+ const row = {
89
+ run_id: input.runId ?? null,
90
+ session_id: input.sessionId ?? null,
91
+ extension_name: input.extensionName,
92
+ command_name: input.commandName,
93
+ args_signature: input.argsSignature,
94
+ args_length: input.argsLength,
95
+ outcome: input.outcome,
96
+ error_code: input.errorCode ?? null,
97
+ duration_ms: input.durationMs,
98
+ details: input.details ?? null,
99
+ pencil_version: BUILD_META.version,
100
+ commit_hash: BUILD_META.commitHash ?? null,
101
+ variant: input.variant ?? null,
102
+ started_at: input.startedAt.toISOString(),
103
+ ended_at: input.endedAt.toISOString(),
104
+ };
105
+ await this.http.postJson(`${this.http.base}/api/database/records/${EXT_COMMAND_EVENTS_TABLE}`, [row]);
106
+ }
107
+ async postLlmCallEvent(input) {
108
+ const row = {
109
+ run_id: input.runId ?? null,
110
+ session_id: input.sessionId ?? null,
111
+ command_event_id: input.commandEventId ?? null,
112
+ extension_name: input.extensionName,
113
+ caller_context: input.callerContext,
114
+ is_user_initiated: input.isUserInitiated,
115
+ model_id: input.modelId ?? null,
116
+ tokens_in: input.tokensIn ?? null,
117
+ tokens_out: input.tokensOut ?? null,
118
+ cost_total: input.costTotal ?? null,
119
+ duration_ms: input.durationMs,
120
+ ok: input.ok,
121
+ error_code: input.errorCode ?? null,
122
+ pencil_version: BUILD_META.version,
123
+ commit_hash: BUILD_META.commitHash ?? null,
124
+ variant: input.variant ?? null,
125
+ started_at: input.startedAt.toISOString(),
126
+ ended_at: input.endedAt.toISOString(),
127
+ };
128
+ await this.http.postJson(`${this.http.base}/api/database/records/${EXT_LLM_CALLS_TABLE}`, [row]);
129
+ }
130
+ async postHookEvent(input) {
131
+ const row = {
132
+ run_id: input.runId ?? null,
133
+ session_id: input.sessionId ?? null,
134
+ extension_name: input.extensionName,
135
+ hook_name: input.hookName,
136
+ duration_ms: input.durationMs,
137
+ ok: input.ok,
138
+ error_code: input.errorCode ?? null,
139
+ sample_rate: input.sampleRate,
140
+ pencil_version: BUILD_META.version,
141
+ variant: input.variant ?? null,
142
+ recorded_at: input.recordedAt.toISOString(),
143
+ };
144
+ await this.http.postJson(`${this.http.base}/api/database/records/${EXT_HOOK_EVENTS_TABLE}`, [row]);
145
+ }
146
+ }
147
+ /**
148
+ * Build a telemetry sink. Returns a noop sink — silently dropping every event —
149
+ * when insforge credentials are missing or disabled. Callers can therefore wire
150
+ * the sink unconditionally; users without credentials pay zero cost.
151
+ */
152
+ export function createExtensionTelemetrySink(options) {
153
+ const creds = loadInsforgeCredentials(options.workspaceRoot, TELEMETRY_SOURCE, options.onDiagnostic);
154
+ const endpoint = creds?.endpoint ?? creds?.insforge_url;
155
+ if (!endpoint || !creds?.api_key || creds.enabled === false) {
156
+ return new NoopExtensionTelemetrySink();
157
+ }
158
+ return new InsforgeExtensionTelemetrySink({
159
+ endpoint,
160
+ apiKey: creds.api_key,
161
+ anonKey: creds.anon_key,
162
+ apiKeyHeader: creds.api_key_header,
163
+ extraHeaders: creds.headers,
164
+ allowSelfSigned: creds.allow_self_signed,
165
+ batchIntervalMs: options.batchIntervalMs,
166
+ onDiagnostic: options.onDiagnostic,
167
+ });
168
+ }
169
+ /**
170
+ * Classify arg text into a small bounded set of signatures so dashboards can
171
+ * group invocations without ingesting user-supplied text. Privacy posture:
172
+ *
173
+ * - "no-args" — empty or whitespace-only args
174
+ * - "--<flag>" — args start with a `--flag` token; we record the flag name only
175
+ * (enum-like, low cardinality, no PII)
176
+ * - "with-args" — any other non-empty args; original text is never recorded
177
+ *
178
+ * Anything else interesting (length, etc.) lives on its own typed column so we
179
+ * never need free-form arg storage.
180
+ */
181
+ export function classifyArgsSignature(args) {
182
+ const trimmed = args.trim();
183
+ if (!trimmed)
184
+ return "no-args";
185
+ const flagMatch = trimmed.match(/^(--[a-z0-9-]+)/i);
186
+ if (flagMatch)
187
+ return flagMatch[1].toLowerCase();
188
+ return "with-args";
189
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * [WHO]: Re-exports the telemetry layer's public surface — types, credential loader, InsforgeHttpClient, BatchingDispatcher
3
+ * [FROM]: Depends on ./types, ./credentials, ./insforge-base, ./batching-dispatcher
4
+ * [TO]: Consumed by extensions/defaults/sal/* and (future) extensions/<ext>/telemetry sinks via `import { ... } from "../../../core/telemetry/index.js"`
5
+ * [HERE]: core/telemetry/index.ts - barrel for the telemetry module; the only entry point external callers should import from
6
+ */
7
+ export type { DiagnosticHandler, InsforgeHttpResult, PostJsonOptions, TelemetryDiagnostic } from "./types.js";
8
+ export { type InsforgeCredentialsBase, loadInsforgeCredentials } from "./credentials.js";
9
+ export { type InsforgeHttpClientOptions, InsforgeHttpClient, parsePostgrestErrorCode, safeHost, } from "./insforge-base.js";
10
+ export { type BatchingDispatcherOptions, BatchingDispatcher } from "./batching-dispatcher.js";
11
+ export { type BuildMeta, loadBuildMeta } from "./build-meta.js";
12
+ export { type CommandEventInput, type CommandOutcome, type CreateExtensionTelemetrySinkOptions, type ExtensionTelemetrySink, type HookEventInput, type LlmCallEventInput, classifyArgsSignature, createExtensionTelemetrySink, HOOK_SAMPLE_RATES, } from "./ext-events.js";
13
+ export { type ExtCallerContext, getExtCallerContext, runWithExtCallerContext } from "./caller-context.js";
@@ -0,0 +1,6 @@
1
+ export { loadInsforgeCredentials } from "./credentials.js";
2
+ export { InsforgeHttpClient, parsePostgrestErrorCode, safeHost, } from "./insforge-base.js";
3
+ export { BatchingDispatcher } from "./batching-dispatcher.js";
4
+ export { loadBuildMeta } from "./build-meta.js";
5
+ export { classifyArgsSignature, createExtensionTelemetrySink, HOOK_SAMPLE_RATES, } from "./ext-events.js";
6
+ export { getExtCallerContext, runWithExtCallerContext } from "./caller-context.js";
@@ -0,0 +1,37 @@
1
+ import type { DiagnosticHandler, InsforgeHttpResult, PostJsonOptions } from "./types.js";
2
+ export interface InsforgeHttpClientOptions {
3
+ endpoint: string;
4
+ apiKey?: string;
5
+ anonKey?: string;
6
+ apiKeyHeader?: string;
7
+ extraHeaders?: Record<string, string>;
8
+ allowSelfSigned?: boolean;
9
+ timeoutMs?: number;
10
+ /** Used as diagnostic source + fingerprint scope. e.g. "sal.eval", "ext.telemetry". */
11
+ source: string;
12
+ onDiagnostic?: DiagnosticHandler;
13
+ }
14
+ /**
15
+ * Thin PostgREST HTTP client. Strictly transport: no routing, no batching, no
16
+ * upsert semantics — those belong to the consumer (e.g. SAL's InsForgeEvalSink).
17
+ *
18
+ * Sinks construct one of these per options.endpoint, then call postJson /
19
+ * patchJson with the full record URL. Diagnostics emit through
20
+ * options.onDiagnostic with `source:category:fingerprint` keying so multiple
21
+ * sinks can share an emit channel without collisions.
22
+ */
23
+ export declare class InsforgeHttpClient {
24
+ readonly base: string;
25
+ private headers;
26
+ private allowSelfSigned;
27
+ private timeoutMs;
28
+ private source;
29
+ private onDiagnostic?;
30
+ constructor(options: InsforgeHttpClientOptions);
31
+ postJson(url: string, body: unknown, extra?: PostJsonOptions): Promise<InsforgeHttpResult>;
32
+ patchJson(url: string, body: unknown): Promise<InsforgeHttpResult>;
33
+ private httpJson;
34
+ private reportDiagnostic;
35
+ }
36
+ export declare function parsePostgrestErrorCode(rawBody: string): string | undefined;
37
+ export declare function safeHost(value: string): string | undefined;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * [WHO]: Provides InsforgeHttpClient class + parsePostgrestErrorCode/safeHost helpers — generic insforge PostgREST transport
3
+ * [FROM]: Depends on node:http (httpRequest), node:https (request), node:url (URL); ./types for DiagnosticHandler / InsforgeHttpResult / PostJsonOptions
4
+ * [TO]: Consumed by extensions/defaults/sal/eval/insforge-sink.ts (SAL-specific routing); future extensions/<ext>/telemetry sinks reuse the same client
5
+ * [HERE]: core/telemetry/insforge-base.ts - factored out of SAL's eval/insforge-sink.ts; HTTP/transport-only, no event semantics, no batching (those live in batching-dispatcher.ts)
6
+ */
7
+ import { request as httpRequest } from "node:http";
8
+ import { request as httpsRequest } from "node:https";
9
+ import { URL } from "node:url";
10
+ import { isDevRuntime } from "../../utils/diagnostics.js";
11
+ const DEFAULT_TIMEOUT_MS = 5000;
12
+ /**
13
+ * Thin PostgREST HTTP client. Strictly transport: no routing, no batching, no
14
+ * upsert semantics — those belong to the consumer (e.g. SAL's InsForgeEvalSink).
15
+ *
16
+ * Sinks construct one of these per options.endpoint, then call postJson /
17
+ * patchJson with the full record URL. Diagnostics emit through
18
+ * options.onDiagnostic with `source:category:fingerprint` keying so multiple
19
+ * sinks can share an emit channel without collisions.
20
+ */
21
+ export class InsforgeHttpClient {
22
+ base;
23
+ headers;
24
+ allowSelfSigned;
25
+ timeoutMs;
26
+ source;
27
+ onDiagnostic;
28
+ constructor(options) {
29
+ this.base = options.endpoint.replace(/\/+$/, "");
30
+ this.allowSelfSigned = options.allowSelfSigned ?? false;
31
+ this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
32
+ this.source = options.source;
33
+ this.onDiagnostic = options.onDiagnostic;
34
+ if (this.allowSelfSigned && isDevRuntime()) {
35
+ console.warn(`[${this.source}] TLS certificate verification disabled (allowSelfSigned=true)`);
36
+ }
37
+ const h = {
38
+ "Content-Type": "application/json",
39
+ ...(options.extraHeaders ?? {}),
40
+ };
41
+ if (options.anonKey) {
42
+ h["apikey"] = options.anonKey;
43
+ h["Authorization"] = `Bearer ${options.anonKey}`;
44
+ }
45
+ if (options.apiKey) {
46
+ h[options.apiKeyHeader ?? "x-api-key"] = options.apiKey;
47
+ if (!options.anonKey) {
48
+ h["Authorization"] = `Bearer ${options.apiKey}`;
49
+ }
50
+ }
51
+ this.headers = h;
52
+ }
53
+ postJson(url, body, extra) {
54
+ const extraHeaders = {};
55
+ if (extra?.prefer)
56
+ extraHeaders["Prefer"] = extra.prefer;
57
+ return this.httpJson("POST", url, body, extraHeaders, extra?.quietErrorCodes);
58
+ }
59
+ patchJson(url, body) {
60
+ return this.httpJson("PATCH", url, body, {});
61
+ }
62
+ httpJson(method, url, body, extraHeaders, quietErrorCodes = []) {
63
+ return new Promise((resolve) => {
64
+ const payload = JSON.stringify(body);
65
+ let parsed;
66
+ try {
67
+ parsed = new URL(url);
68
+ }
69
+ catch {
70
+ this.reportDiagnostic("config", "Insforge endpoint URL is invalid.", { url }, "invalid-url");
71
+ resolve({ ok: false });
72
+ return;
73
+ }
74
+ const isHttps = parsed.protocol === "https:";
75
+ const requestFn = isHttps ? httpsRequest : httpRequest;
76
+ const port = parsed.port ? Number(parsed.port) : isHttps ? 443 : 80;
77
+ const req = requestFn({
78
+ hostname: parsed.hostname,
79
+ port,
80
+ path: parsed.pathname + parsed.search,
81
+ method,
82
+ headers: {
83
+ ...this.headers,
84
+ ...extraHeaders,
85
+ "Content-Length": Buffer.byteLength(payload),
86
+ },
87
+ timeout: this.timeoutMs,
88
+ ...(isHttps && this.allowSelfSigned ? { rejectUnauthorized: false } : {}),
89
+ }, (res) => {
90
+ let rawBody = "";
91
+ res.setEncoding("utf-8");
92
+ res.on("data", (chunk) => {
93
+ rawBody += chunk;
94
+ });
95
+ res.on("end", () => {
96
+ const ok = res.statusCode !== undefined && res.statusCode < 300;
97
+ const errorCode = parsePostgrestErrorCode(rawBody);
98
+ if (!ok && !quietErrorCodes.includes(errorCode ?? "")) {
99
+ this.reportDiagnostic(errorCode === "PGRST204" ? "schema" : "network", `Insforge upload failed with HTTP ${res.statusCode}.`, {
100
+ method,
101
+ path: parsed.pathname,
102
+ statusCode: res.statusCode,
103
+ body: rawBody.slice(0, 300),
104
+ errorCode,
105
+ }, `http-${res.statusCode ?? "unknown"}-${errorCode ?? "none"}`);
106
+ }
107
+ resolve({ ok, statusCode: res.statusCode, body: rawBody, errorCode });
108
+ });
109
+ });
110
+ req.on("error", (err) => {
111
+ this.reportDiagnostic("network", "Insforge upload is failing due to a network connection error.", { host: parsed.hostname, error: err.message }, "network-error");
112
+ if (isDevRuntime()) {
113
+ console.error(`[${this.source}] network error → ${parsed.hostname}: ${err.message}`);
114
+ }
115
+ resolve({ ok: false });
116
+ });
117
+ req.on("timeout", () => {
118
+ this.reportDiagnostic("network", "Insforge upload timed out.", { method, path: parsed.pathname, host: parsed.hostname }, "timeout");
119
+ if (isDevRuntime()) {
120
+ console.error(`[${this.source}] timeout ${method} ${parsed.pathname}`);
121
+ }
122
+ req.destroy();
123
+ resolve({ ok: false });
124
+ });
125
+ req.write(payload);
126
+ req.end();
127
+ });
128
+ }
129
+ reportDiagnostic(category, message, detail, fingerprintSuffix) {
130
+ this.onDiagnostic?.({
131
+ source: this.source,
132
+ severity: category === "config" ? "warning" : "error",
133
+ category,
134
+ message,
135
+ detail,
136
+ fingerprint: `${this.source}:${category}:${fingerprintSuffix}`,
137
+ context: {
138
+ adapter: "insforge",
139
+ endpoint_host: safeHost(this.base),
140
+ },
141
+ });
142
+ }
143
+ }
144
+ export function parsePostgrestErrorCode(rawBody) {
145
+ try {
146
+ const parsed = JSON.parse(rawBody);
147
+ return typeof parsed?.code === "string" ? parsed.code : undefined;
148
+ }
149
+ catch {
150
+ return undefined;
151
+ }
152
+ }
153
+ export function safeHost(value) {
154
+ try {
155
+ return new URL(value).hostname;
156
+ }
157
+ catch {
158
+ return undefined;
159
+ }
160
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * [WHO]: Provides TelemetryDiagnostic event shape, DiagnosticHandler callback, InsforgeHttpResult, PostJsonOptions — shared types for all telemetry sinks
3
+ * [FROM]: No internal deps; only TypeScript-level type primitives
4
+ * [TO]: Consumed by core/telemetry/credentials.ts, insforge-base.ts, batching-dispatcher.ts; mirrored by sinks (SAL eval, future extension telemetry)
5
+ * [HERE]: core/telemetry/types.ts - foundational type surface for the telemetry layer; identical diagnostic shape to SAL's onDiagnostic so SAL can adopt without callsite churn
6
+ */
7
+ /**
8
+ * Diagnostic event emitted by any telemetry component. The shape is intentionally
9
+ * identical to SAL's eval onDiagnostic signature so SAL adopts the base layer
10
+ * without changing any of its existing diagnostic handlers.
11
+ */
12
+ export interface TelemetryDiagnostic {
13
+ source: string;
14
+ severity: "debug" | "info" | "warning" | "error";
15
+ category: "network" | "fallback" | "persistence" | "config" | "extension_timeout" | "schema" | "unknown";
16
+ message: string;
17
+ detail?: unknown;
18
+ fingerprint?: string;
19
+ context?: Record<string, unknown>;
20
+ }
21
+ export type DiagnosticHandler = (event: TelemetryDiagnostic) => void;
22
+ export interface InsforgeHttpResult {
23
+ ok: boolean;
24
+ statusCode?: number;
25
+ body?: string;
26
+ errorCode?: string;
27
+ }
28
+ export interface PostJsonOptions {
29
+ /** PostgREST Prefer header. e.g. "resolution=merge-duplicates". */
30
+ prefer?: string;
31
+ /** Error codes (e.g. "PGRST204") to suppress from onDiagnostic. */
32
+ quietErrorCodes?: string[];
33
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * [WHO]: Provides TelemetryDiagnostic event shape, DiagnosticHandler callback, InsforgeHttpResult, PostJsonOptions — shared types for all telemetry sinks
3
+ * [FROM]: No internal deps; only TypeScript-level type primitives
4
+ * [TO]: Consumed by core/telemetry/credentials.ts, insforge-base.ts, batching-dispatcher.ts; mirrored by sinks (SAL eval, future extension telemetry)
5
+ * [HERE]: core/telemetry/types.ts - foundational type surface for the telemetry layer; identical diagnostic shape to SAL's onDiagnostic so SAL can adopt without callsite churn
6
+ */
7
+ export {};
@@ -18,6 +18,14 @@ const HARNESS_SRC_PATH = join(__dirname, "src");
18
18
  const INTERACTION_SKILLS_PATH = join(__dirname, "interaction-skills");
19
19
  const WORKSPACE_TEMPLATE_PATH = join(__dirname, "agent-workspace");
20
20
  const DEFAULT_TIMEOUT_SECONDS = 120;
21
+ const BROWSER_COMMAND_COMPLETIONS = [
22
+ { value: "install", label: "install", description: "Install Browser Harness Python dependencies" },
23
+ { value: "status", label: "status", description: "Run doctor diagnostics" },
24
+ { value: "setup", label: "setup", description: "Attach to the running browser" },
25
+ { value: "reload", label: "reload", description: "Restart the Browser Harness daemon" },
26
+ { value: "workspace", label: "workspace", description: "Show the helper workspace path" },
27
+ { value: "help", label: "help", description: "Show Browser Harness commands" },
28
+ ];
21
29
  const BrowserRunInputSchema = Type.Object({
22
30
  code: Type.String({
23
31
  description: "Python code to execute with Browser Harness helpers pre-imported. Use new_tab(url), wait_for_load(), page_info(), capture_screenshot(), click_at_xy(), js(), cdp(), etc.",
@@ -328,12 +336,12 @@ export default function browserExtension(api) {
328
336
  ensureBrowserWorkspace();
329
337
  });
330
338
  api.registerCommand("browser", {
331
- description: "Browser Harness setup, status, reload, and workspace information",
332
- getArgumentCompletions: (argumentPrefix) => {
333
- const prefix = argumentPrefix.trim();
334
- const values = ["install", "status", "setup", "reload", "workspace", "help"]
335
- .filter((value) => value.startsWith(prefix))
336
- .map((value) => ({ value, label: value }));
339
+ description: "Set up or inspect browser automation tools",
340
+ getArgumentCompletions: (argumentPrefix, context) => {
341
+ if (context && context.tokenIndex > 0)
342
+ return null;
343
+ const prefix = argumentPrefix.trim().toLowerCase();
344
+ const values = BROWSER_COMMAND_COMPLETIONS.filter((item) => item.value.startsWith(prefix));
337
345
  return values.length > 0 ? values : null;
338
346
  },
339
347
  handler: async (args, ctx) => {
@@ -14,7 +14,7 @@ Rules:
14
14
  - Do NOT use any tools
15
15
  - Do NOT say "I cannot" or explain limitations
16
16
  - If you don't know, say so briefly
17
- - Keep responses short (1-3 sentences unless detail is critical`;
17
+ - Keep responses short (1-3 sentences unless detail is critical).`;
18
18
  const MAX_TURN_CHARS = 500;
19
19
  function extractText(content) {
20
20
  if (typeof content === "string")
@@ -111,7 +111,7 @@ export default async function btwExtension(api) {
111
111
  });
112
112
  // Register /btw command
113
113
  api.registerCommand("btw", {
114
- description: "Ask a quick question without interrupting the current task",
114
+ description: "Ask a quick side question while the main task keeps its place",
115
115
  handler: (args, ctx) => handleBtwCommand(args, ctx, api),
116
116
  });
117
117
  }
@@ -5,7 +5,7 @@
5
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
- import { collectSystemInfo, collectModelInfo, collectSessionInfo, collectConfigInfo, collectGitInfo, collectAgentState, sanitizeForLLM, formatDiagnosticData, } from "./collectors.js";
8
+ import { collectSystemInfo, collectModelInfo, collectSessionInfo, collectConfigInfo, collectGitInfo, collectAgentState, collectPreferencesInfo, sanitizeForLLM, formatDiagnosticData, } from "./collectors.js";
9
9
  const DEBUG_MESSAGE_TYPE = "debug";
10
10
  const DEBUG_PROMPT_PREFIX = "[DEBUG:";
11
11
  const DEBUG_TAG = "[DEBUG]";
@@ -47,6 +47,16 @@ let pendingDiagnosticPrompt;
47
47
  function isDebugPrompt(text) {
48
48
  return text.startsWith(DEBUG_PROMPT_PREFIX);
49
49
  }
50
+ const DEBUG_COMMAND_COMPLETIONS = [
51
+ { value: "env", label: "env", description: "Show terminal and system details" },
52
+ { value: "session", label: "session", description: "Show current conversation state" },
53
+ { value: "model", label: "model", description: "Show active model and provider details" },
54
+ { value: "preferences", label: "preferences", description: "Show saved preferences" },
55
+ ];
56
+ const LOCALE_COMMAND_COMPLETIONS = [
57
+ { value: "zh", label: "zh", description: "Use Chinese for future replies" },
58
+ { value: "en", label: "en", description: "Use English for future replies" },
59
+ ];
50
60
  function parseDebugArgs(args) {
51
61
  const trimmed = args.trim().toLowerCase();
52
62
  if (trimmed === "env")
@@ -55,6 +65,8 @@ function parseDebugArgs(args) {
55
65
  return { subcommand: "session" };
56
66
  if (trimmed === "model")
57
67
  return { subcommand: "model" };
68
+ if (trimmed === "preferences")
69
+ return { subcommand: "preferences" };
58
70
  return { subcommand: "full", issueDescription: args.trim() || undefined };
59
71
  }
60
72
  // ============================================================================
@@ -132,6 +144,15 @@ async function handleQuickSub(subcommand, ctx, api) {
132
144
  : `> Collection failed: ${info.error}`;
133
145
  break;
134
146
  }
147
+ case "preferences": {
148
+ const info = await collectPreferencesInfo(ctx);
149
+ result = info.data
150
+ ? `| Preferences | |\n|---|---|\n${Object.entries(info.data)
151
+ .map(([k, v]) => `| ${k} | ${typeof v === "string" ? v : JSON.stringify(v)} |`)
152
+ .join("\n")}`
153
+ : `> Collection failed: ${info.error}`;
154
+ break;
155
+ }
135
156
  }
136
157
  api.sendMessage({
137
158
  customType: DEBUG_MESSAGE_TYPE,
@@ -170,7 +191,7 @@ export default async function debugExtension(api) {
170
191
  return container;
171
192
  });
172
193
  api.on("before_agent_start", (event) => {
173
- if (!isDebugPrompt(event.prompt))
194
+ if (!isDebugPrompt(event.prompt) || event.prompt !== pendingDiagnosticPrompt)
174
195
  return;
175
196
  return { appendSystemPrompt: DEBUG_SYSTEM_PROMPT };
176
197
  });
@@ -180,12 +201,26 @@ export default async function debugExtension(api) {
180
201
  }
181
202
  });
182
203
  api.registerCommand("debug", {
183
- description: "Run system diagnostics (/debug [env|session|model|preferences|<issue>])",
204
+ description: "Check NanoPencil health or investigate an issue. Usage: /debug [env|session|model|preferences|<issue>]",
205
+ getArgumentCompletions: (argumentPrefix, context) => {
206
+ if (context && context.tokenIndex > 0)
207
+ return null;
208
+ const prefix = argumentPrefix.trim().toLowerCase();
209
+ const values = DEBUG_COMMAND_COMPLETIONS.filter((value) => value.value.startsWith(prefix));
210
+ return values.length > 0 ? values : null;
211
+ },
184
212
  handler: (args, ctx) => handleDebugCommand(args, ctx, api),
185
213
  });
186
214
  // Register /set-locale command
187
215
  api.registerCommand("set-locale", {
188
216
  description: "Set language preference (/set-locale zh|en)",
217
+ getArgumentCompletions: (argumentPrefix, context) => {
218
+ if (context && context.tokenIndex > 0)
219
+ return null;
220
+ const prefix = argumentPrefix.trim().toLowerCase();
221
+ const values = LOCALE_COMMAND_COMPLETIONS.filter((value) => value.value.startsWith(prefix));
222
+ return values.length > 0 ? values : null;
223
+ },
189
224
  handler: async (args, ctx) => {
190
225
  const trimmed = args.trim().toLowerCase();
191
226
  if (trimmed !== "zh" && trimmed !== "en") {
@@ -10,6 +10,17 @@ import { coerceDiagnosticEvent, DiagnosticBuffer } from "./diagnostic-buffer.js"
10
10
  import { reportDiagnostics } from "./reporter.js";
11
11
  import { DIAGNOSTIC_EVENT_CHANNEL } from "./types.js";
12
12
  const MESSAGE_TYPE = "diagnostics";
13
+ const REPORT_ISSUE_COMPLETIONS = [
14
+ { value: "last", label: "last", description: "Report the latest diagnostic" },
15
+ { value: "all", label: "all", description: "Report all diagnostics from this session" },
16
+ ];
17
+ function getReportIssueArgumentCompletions(argumentPrefix, context) {
18
+ if (context && context.tokenIndex > 0)
19
+ return null;
20
+ const prefix = argumentPrefix.trim().toLowerCase();
21
+ const values = REPORT_ISSUE_COMPLETIONS.filter((item) => item.value.startsWith(prefix));
22
+ return values.length > 0 ? values.map((item) => ({ ...item })) : null;
23
+ }
13
24
  export default async function diagnosticsExtension(api) {
14
25
  const buffer = new DiagnosticBuffer();
15
26
  api.registerMessageRenderer(MESSAGE_TYPE, (message, _options, theme) => {
@@ -49,6 +60,7 @@ export default async function diagnosticsExtension(api) {
49
60
  });
50
61
  api.registerCommand("report-issue", {
51
62
  description: "Report recent diagnostics (/report-issue [last|all|note])",
63
+ getArgumentCompletions: getReportIssueArgumentCompletions,
52
64
  handler: (args, ctx) => handleReportIssue(args, ctx, buffer),
53
65
  });
54
66
  }
@@ -1,10 +1,24 @@
1
1
  /**
2
- * [WHO]: parseGrubCommand, buildGrubHelp
2
+ * [WHO]: parseGrubCommand, buildGrubHelp, getGrubArgumentCompletions
3
3
  * [FROM]: Depends on ./grub-types
4
4
  * [TO]: Consumed by extension entry point (./index.ts)
5
5
  * [HERE]: extensions/defaults/grub/grub-parser.ts - /grub command parser with resume/status --json/--max-iter/--max-fail flags
6
6
  */
7
7
  import type { ParsedGrubCommand } from "./grub-types.js";
8
8
  import { type GrubLocale } from "./grub-i18n.js";
9
+ type GrubArgumentCompletionContext = {
10
+ commandName: string;
11
+ argumentText: string;
12
+ argumentPrefix: string;
13
+ tokenIndex: number;
14
+ previousTokens: string[];
15
+ };
16
+ type GrubCompletionItem = {
17
+ value: string;
18
+ label: string;
19
+ description: string;
20
+ };
21
+ export declare function getGrubArgumentCompletions(argumentPrefix: string, context?: GrubArgumentCompletionContext): GrubCompletionItem[] | null;
9
22
  export declare function parseGrubCommand(input: string): ParsedGrubCommand;
10
23
  export declare function buildGrubHelp(reason?: string, locale?: GrubLocale): string;
24
+ export {};