@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
@@ -1,40 +1,89 @@
1
1
  export const BUILTIN_SLASH_COMMANDS = [
2
- { name: "settings", descriptionKey: "slash.settings" },
3
- { name: "model", descriptionKey: "slash.model" },
4
- { name: "agent-loop", descriptionKey: "slash.agent-loop" },
5
- { name: "scoped-models", descriptionKey: "slash.scoped-models" },
6
- { name: "apikey", descriptionKey: "slash.apikey" },
7
- { name: "mcp", descriptionKey: "slash.mcp" },
8
- { name: "soul", descriptionKey: "slash.soul" },
9
- { name: "persona", descriptionKey: "slash.persona" },
10
- { name: "memory", descriptionKey: "slash.memory" },
11
- { name: "dream", descriptionKey: "slash.dream" },
12
- { name: "export", descriptionKey: "slash.export" },
13
- { name: "share", descriptionKey: "slash.share" },
14
- { name: "copy", descriptionKey: "slash.copy" },
15
- { name: "name", descriptionKey: "slash.name" },
16
- { name: "session", descriptionKey: "slash.session" },
17
- { name: "status", descriptionKey: "slash.status" },
18
- { name: "usage", descriptionKey: "slash.usage" },
19
- { name: "changelog", descriptionKey: "slash.changelog" },
20
- { name: "hotkeys", descriptionKey: "slash.hotkeys" },
21
- { name: "fork", descriptionKey: "slash.fork" },
22
- { name: "tree", descriptionKey: "slash.tree" },
23
- { name: "login", descriptionKey: "slash.login" },
24
- { name: "logout", descriptionKey: "slash.logout" },
25
- { name: "new", descriptionKey: "slash.new" },
26
- { name: "update", descriptionKey: "slash.update" },
27
- { name: "reinstall", descriptionKey: "slash.reinstall" },
28
- { name: "compact", descriptionKey: "slash.compact" },
29
- { name: "resume", descriptionKey: "slash.resume" },
30
- { name: "reload", descriptionKey: "slash.reload" },
31
- { name: "link-world", descriptionKey: "slash.link-world" },
32
- { name: "language", descriptionKey: "slash.language" },
33
- { name: "quit", descriptionKey: "slash.quit" },
2
+ { name: "settings", descriptionKey: "slash.settings", category: "core" },
3
+ { name: "model", descriptionKey: "slash.model", category: "model" },
4
+ { name: "thinking", descriptionKey: "slash.thinking", category: "model" },
5
+ { name: "agent-loop", descriptionKey: "slash.agent-loop", category: "model" },
6
+ { name: "scoped-models", descriptionKey: "slash.scoped-models", category: "model" },
7
+ { name: "apikey", descriptionKey: "slash.apikey", category: "model" },
8
+ { name: "mcp", descriptionKey: "slash.mcp", category: "tools" },
9
+ { name: "soul", descriptionKey: "slash.soul", category: "memory" },
10
+ { name: "persona", descriptionKey: "slash.persona", category: "core" },
11
+ { name: "memory", descriptionKey: "slash.memory", category: "memory" },
12
+ { name: "dream", descriptionKey: "slash.dream", category: "memory", implementation: "extension" },
13
+ { name: "export", descriptionKey: "slash.export", category: "tools" },
14
+ { name: "share", descriptionKey: "slash.share", category: "tools" },
15
+ { name: "copy", descriptionKey: "slash.copy", category: "core" },
16
+ { name: "name", descriptionKey: "slash.name", category: "session" },
17
+ { name: "session", descriptionKey: "slash.session", category: "session" },
18
+ { name: "status", descriptionKey: "slash.status", category: "core" },
19
+ { name: "usage", descriptionKey: "slash.usage", category: "core" },
20
+ { name: "changelog", descriptionKey: "slash.changelog", category: "core" },
21
+ { name: "hotkeys", descriptionKey: "slash.hotkeys", category: "core" },
22
+ { name: "resources", descriptionKey: "slash.resources", category: "core" },
23
+ { name: "fork", descriptionKey: "slash.fork", category: "session" },
24
+ { name: "tree", descriptionKey: "slash.tree", category: "session" },
25
+ { name: "login", descriptionKey: "slash.login", category: "model" },
26
+ { name: "logout", descriptionKey: "slash.logout", category: "model" },
27
+ { name: "new", descriptionKey: "slash.new", category: "session" },
28
+ { name: "update", descriptionKey: "slash.update", category: "admin" },
29
+ { name: "reinstall", descriptionKey: "slash.reinstall", category: "admin" },
30
+ { name: "compact", descriptionKey: "slash.compact", category: "session" },
31
+ { name: "resume", descriptionKey: "slash.resume", category: "session" },
32
+ { name: "reload", descriptionKey: "slash.reload", category: "admin" },
33
+ { name: "link-world", descriptionKey: "slash.link-world", category: "tools", implementation: "extension" },
34
+ { name: "language", descriptionKey: "slash.language", category: "core" },
35
+ { name: "quit", descriptionKey: "slash.quit", category: "core" },
34
36
  ];
37
+ export function getExtensionBackedBuiltinCommandNames() {
38
+ return new Set(BUILTIN_SLASH_COMMANDS.filter((command) => command.implementation === "extension").map((command) => command.name));
39
+ }
40
+ export function inferSlashCommandCategory(name, source) {
41
+ if (source === "prompt")
42
+ return "workflow";
43
+ if (source === "skill")
44
+ return "tools";
45
+ if (name === "dream" || name === "memory" || name.startsWith("mem-"))
46
+ return "memory";
47
+ if (name === "team" || name.startsWith("team:") || name === "subagent" || name.startsWith("subagent:"))
48
+ return "agents";
49
+ if (name === "grub" ||
50
+ name === "loop" ||
51
+ name === "plan" ||
52
+ name.startsWith("plan:") ||
53
+ name === "recap" ||
54
+ name === "btw" ||
55
+ name === "interview" ||
56
+ name === "grill-me" ||
57
+ name === "simplify") {
58
+ return "workflow";
59
+ }
60
+ if (name === "browser" || name === "figma" || name === "link-world" || name === "export")
61
+ return "tools";
62
+ if (name === "debug" ||
63
+ name === "set-locale" ||
64
+ name === "report-issue" ||
65
+ name === "tokensave" ||
66
+ name === "security" ||
67
+ name.startsWith("security-") ||
68
+ name.startsWith("sal:")) {
69
+ return "admin";
70
+ }
71
+ return "tools";
72
+ }
73
+ export function getSlashCommandCategoryLabel(category, t) {
74
+ return t(`slash.categories.${category}`);
75
+ }
76
+ export function formatSlashCommandDescription(description, category, t) {
77
+ if (!description || !category)
78
+ return description;
79
+ const label = getSlashCommandCategoryLabel(category, t);
80
+ return label && !label.startsWith("slash.") ? `${label} · ${description}` : description;
81
+ }
35
82
  export function getLocalizedCommands(t) {
36
83
  return BUILTIN_SLASH_COMMANDS.map((cmd) => ({
37
84
  name: cmd.name,
38
85
  description: t(cmd.descriptionKey),
86
+ category: cmd.category,
87
+ categoryLabel: getSlashCommandCategoryLabel(cmd.category, t),
39
88
  }));
40
89
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * [WHO]: Provides BatchingDispatcher<T> — generic event-buffering with deferred-flush timer, reentrancy protection, and close-time drain
3
+ * [FROM]: Depends on ./types for DiagnosticHandler; sink-agnostic — no HTTP, no insforge, no SAL semantics
4
+ * [TO]: Consumed by extensions/defaults/sal/eval/insforge-sink.ts via composition; future ext-telemetry sink reuses the same machinery
5
+ * [HERE]: core/telemetry/batching-dispatcher.ts - factored out of SAL's eval sink: SAL's scheduleFlush/doFlush/flush/close logic was inherently generic, now reusable
6
+ */
7
+ import type { DiagnosticHandler } from "./types.js";
8
+ export interface BatchingDispatcherOptions<T> {
9
+ /** Per-event work. Errors are caught and reported via onDiagnostic. */
10
+ handler: (event: T) => Promise<void>;
11
+ /** Flush timer interval in ms. Default 2000ms. */
12
+ intervalMs?: number;
13
+ /** Diagnostic source used in fingerprints. e.g. "sal.eval". */
14
+ source: string;
15
+ onDiagnostic?: DiagnosticHandler;
16
+ }
17
+ /**
18
+ * Generic buffer-then-drain dispatcher. Consumers call enqueue() to add work;
19
+ * a debounced flush timer drains the queue serially through the handler.
20
+ * close() drains synchronously (best-effort) before letting the process exit.
21
+ *
22
+ * Reentrancy: doFlush() loops over splice()'d batches so events arriving while
23
+ * a flush is in flight are picked up by the same flush cycle.
24
+ */
25
+ export declare class BatchingDispatcher<T> {
26
+ private pending;
27
+ private flushTimer;
28
+ private flushInFlight;
29
+ private closed;
30
+ private handler;
31
+ private intervalMs;
32
+ private source;
33
+ private onDiagnostic?;
34
+ constructor(options: BatchingDispatcherOptions<T>);
35
+ enqueue(event: T): void;
36
+ flush(): Promise<void>;
37
+ close(): Promise<void>;
38
+ private scheduleFlush;
39
+ private doFlush;
40
+ private reportFlushFailure;
41
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Generic buffer-then-drain dispatcher. Consumers call enqueue() to add work;
3
+ * a debounced flush timer drains the queue serially through the handler.
4
+ * close() drains synchronously (best-effort) before letting the process exit.
5
+ *
6
+ * Reentrancy: doFlush() loops over splice()'d batches so events arriving while
7
+ * a flush is in flight are picked up by the same flush cycle.
8
+ */
9
+ export class BatchingDispatcher {
10
+ pending = [];
11
+ flushTimer;
12
+ flushInFlight;
13
+ closed = false;
14
+ handler;
15
+ intervalMs;
16
+ source;
17
+ onDiagnostic;
18
+ constructor(options) {
19
+ this.handler = options.handler;
20
+ this.intervalMs = options.intervalMs ?? 2000;
21
+ this.source = options.source;
22
+ this.onDiagnostic = options.onDiagnostic;
23
+ }
24
+ enqueue(event) {
25
+ if (this.closed)
26
+ return;
27
+ this.pending.push(event);
28
+ this.scheduleFlush();
29
+ }
30
+ async flush() {
31
+ if (this.flushInFlight) {
32
+ await this.flushInFlight.catch(() => { });
33
+ return;
34
+ }
35
+ this.flushInFlight = this.doFlush();
36
+ try {
37
+ await this.flushInFlight;
38
+ }
39
+ catch (err) {
40
+ this.reportFlushFailure(err, "flush");
41
+ }
42
+ finally {
43
+ this.flushInFlight = undefined;
44
+ }
45
+ }
46
+ async close() {
47
+ this.closed = true;
48
+ await this.flush().catch((err) => {
49
+ this.reportFlushFailure(err, "close-flush");
50
+ });
51
+ }
52
+ scheduleFlush() {
53
+ if (this.flushTimer)
54
+ return;
55
+ this.flushTimer = setTimeout(() => {
56
+ this.flushTimer = undefined;
57
+ void this.flush().catch(() => { });
58
+ }, this.intervalMs);
59
+ }
60
+ async doFlush() {
61
+ if (this.flushTimer) {
62
+ clearTimeout(this.flushTimer);
63
+ this.flushTimer = undefined;
64
+ }
65
+ while (true) {
66
+ const toFlush = this.pending.splice(0);
67
+ if (toFlush.length === 0)
68
+ break;
69
+ for (const event of toFlush) {
70
+ try {
71
+ await this.handler(event);
72
+ }
73
+ catch (err) {
74
+ this.reportFlushFailure(err, "handler");
75
+ }
76
+ }
77
+ }
78
+ }
79
+ reportFlushFailure(err, fingerprintSuffix) {
80
+ this.onDiagnostic?.({
81
+ source: this.source,
82
+ severity: "error",
83
+ category: "persistence",
84
+ message: "Telemetry flush failed.",
85
+ detail: err instanceof Error ? { error: err.message } : err,
86
+ fingerprint: `${this.source}:persistence:${fingerprintSuffix}`,
87
+ });
88
+ }
89
+ }
@@ -0,0 +1,12 @@
1
+ export interface BuildMeta {
2
+ version: string;
3
+ commitHash?: string;
4
+ branch?: string;
5
+ }
6
+ /**
7
+ * Walk up from this module's location looking for either a generated
8
+ * `build-meta.json` (production / dist) or the project's `package.json` (dev /
9
+ * tsx). Location-independent: works regardless of which directory the caller
10
+ * lives in. Always returns a result — fallback is `{ version: "dev" }`.
11
+ */
12
+ export declare function loadBuildMeta(): BuildMeta;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * [WHO]: Provides BuildMeta interface + loadBuildMeta() — single-source build-stamp loader (version + commit + branch)
3
+ * [FROM]: Depends on node:fs (existsSync, readFileSync), node:path (dirname, join), node:url (fileURLToPath)
4
+ * [TO]: Consumed by extensions/defaults/sal/index.ts and core/telemetry/ext-events.ts; both want the same version stamp on every emitted row
5
+ * [HERE]: core/telemetry/build-meta.ts - extracted from SAL's inline loadBuildMeta(); location-independent walker so callers in core/, extensions/, or scripts/ all resolve the same way
6
+ */
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { dirname, join } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ const FALLBACK = { version: "dev" };
11
+ const PKG_NAME = "@pencil-agent/nano-pencil";
12
+ const MAX_WALK_DEPTH = 12;
13
+ /**
14
+ * Walk up from this module's location looking for either a generated
15
+ * `build-meta.json` (production / dist) or the project's `package.json` (dev /
16
+ * tsx). Location-independent: works regardless of which directory the caller
17
+ * lives in. Always returns a result — fallback is `{ version: "dev" }`.
18
+ */
19
+ export function loadBuildMeta() {
20
+ try {
21
+ const thisFile = fileURLToPath(import.meta.url);
22
+ let dir = dirname(thisFile);
23
+ for (let i = 0; i < MAX_WALK_DEPTH; i++) {
24
+ const distMetaPath = join(dir, "build-meta.json");
25
+ if (existsSync(distMetaPath)) {
26
+ const parsed = JSON.parse(readFileSync(distMetaPath, "utf-8"));
27
+ return {
28
+ version: typeof parsed.version === "string" ? parsed.version : FALLBACK.version,
29
+ commitHash: typeof parsed.commitHash === "string" ? parsed.commitHash : undefined,
30
+ branch: typeof parsed.branch === "string" ? parsed.branch : undefined,
31
+ };
32
+ }
33
+ const pkgPath = join(dir, "package.json");
34
+ if (existsSync(pkgPath)) {
35
+ try {
36
+ const parsed = JSON.parse(readFileSync(pkgPath, "utf-8"));
37
+ if (parsed?.name === PKG_NAME) {
38
+ return {
39
+ version: typeof parsed.version === "string" ? parsed.version : FALLBACK.version,
40
+ };
41
+ }
42
+ }
43
+ catch {
44
+ // fall through and keep walking
45
+ }
46
+ }
47
+ const parent = dirname(dir);
48
+ if (parent === dir)
49
+ break;
50
+ dir = parent;
51
+ }
52
+ }
53
+ catch {
54
+ // non-fatal — fall through to fallback
55
+ }
56
+ return FALLBACK;
57
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Per-async-frame metadata identifying which extension triggered the current
3
+ * execution path and whether that path was initiated by a user action (slash
4
+ * command) or by a hook auto-firing. Read by the LLM-call telemetry wrapper
5
+ * so each ext_llm_calls row can answer "who burned these tokens, and was the
6
+ * user aware?".
7
+ */
8
+ export interface ExtCallerContext {
9
+ extensionName: string;
10
+ /**
11
+ * Short scope label, e.g. "command:/recap --smart" or "hook:before_agent_start".
12
+ * Format is consumer-readable; bounded length (≤128 chars).
13
+ */
14
+ callerContext: string;
15
+ /**
16
+ * True when the path was initiated by the user typing a slash command;
17
+ * false when the path was initiated by an extension hook auto-firing.
18
+ * This is the field SQL dashboards group on to catch idle-thinking-class
19
+ * bugs (hooks silently calling LLMs the user never asked for).
20
+ */
21
+ isUserInitiated: boolean;
22
+ sessionId?: string | null;
23
+ runId?: string | null;
24
+ variant?: string | null;
25
+ }
26
+ /**
27
+ * Run `fn` with `ctx` accessible to any descendant async frame via
28
+ * getExtCallerContext(). The context is automatically cleared when the
29
+ * returned promise settles.
30
+ */
31
+ export declare function runWithExtCallerContext<T>(ctx: ExtCallerContext, fn: () => Promise<T> | T): Promise<T>;
32
+ export declare function getExtCallerContext(): ExtCallerContext | undefined;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * [WHO]: Provides ExtCallerContext interface, runWithExtCallerContext(), getExtCallerContext() — AsyncLocalStorage-backed caller-attribution bus
3
+ * [FROM]: Depends on node:async_hooks (AsyncLocalStorage); no other internal deps to stay loadable from anywhere in the runtime
4
+ * [TO]: runWithExtCallerContext is pushed by core/extensions/runner.ts at every command/hook dispatch boundary; getExtCallerContext is read by core/runtime/extension-core-bindings.ts when writing ext_llm_calls rows
5
+ * [HERE]: core/telemetry/caller-context.ts - the only mechanism distinguishing "extension command path (user-initiated)" from "extension hook path (auto-fired)" when an LLM call eventually happens; if this bus is empty, the telemetry layer marks the call as is_user_initiated=false / extension_name="unknown"
6
+ */
7
+ import { AsyncLocalStorage } from "node:async_hooks";
8
+ const storage = new AsyncLocalStorage();
9
+ /**
10
+ * Run `fn` with `ctx` accessible to any descendant async frame via
11
+ * getExtCallerContext(). The context is automatically cleared when the
12
+ * returned promise settles.
13
+ */
14
+ export function runWithExtCallerContext(ctx, fn) {
15
+ return Promise.resolve(storage.run(ctx, fn));
16
+ }
17
+ export function getExtCallerContext() {
18
+ return storage.getStore();
19
+ }
@@ -0,0 +1,27 @@
1
+ import type { DiagnosticHandler } from "./types.js";
2
+ /**
3
+ * Base shape of an insforge credentials record. Sinks may extend this with
4
+ * their own fields (e.g. SAL adds `cleanup_stale_runs`); use the generic
5
+ * parameter on loadInsforgeCredentials to type those extra fields safely.
6
+ */
7
+ export interface InsforgeCredentialsBase {
8
+ endpoint?: string;
9
+ /** Legacy alias for `endpoint` — both formats are produced by older configs. */
10
+ insforge_url?: string;
11
+ api_key?: string;
12
+ anon_key?: string;
13
+ api_key_header?: string;
14
+ headers?: Record<string, string>;
15
+ allow_self_signed?: boolean;
16
+ enabled?: boolean;
17
+ }
18
+ /**
19
+ * Walks a prioritized list of candidate paths (workspace overrides home dir) and
20
+ * returns the first usable credentials record. Returns null when nothing is found
21
+ * or all files are unparseable.
22
+ *
23
+ * `source` is folded into emitted diagnostics so SAL keeps emitting
24
+ * `sal.eval:...` fingerprints and a future ext-telemetry sink emits
25
+ * `ext.telemetry:...` without changing this loader.
26
+ */
27
+ export declare function loadInsforgeCredentials<T extends InsforgeCredentialsBase = InsforgeCredentialsBase>(workspaceRoot: string, source: string, onDiagnostic?: DiagnosticHandler, extraEnvPath?: string): T | null;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * [WHO]: Provides InsforgeCredentialsBase interface + loadInsforgeCredentials() — single-source credential loader for any insforge-backed telemetry sink
3
+ * [FROM]: Depends on node:fs (existsSync, readFileSync), node:os (homedir), node:path (join), ./types for DiagnosticHandler
4
+ * [TO]: Consumed by extensions/defaults/sal/index.ts via re-export; future extension-telemetry sink reads via the same loader
5
+ * [HERE]: core/telemetry/credentials.ts - parses ~/.memory-experiments/credentials.json (workspace fallback ordered first), accepts both {credentials:[{id,...}]} and flat formats, returns null when no usable file found
6
+ */
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+ const CREDENTIALS_BASENAME = ".memory-experiments";
11
+ const CREDENTIALS_FILE = "credentials.json";
12
+ const INSFORGE_ENTRY_ID = "insforge";
13
+ /**
14
+ * Walks a prioritized list of candidate paths (workspace overrides home dir) and
15
+ * returns the first usable credentials record. Returns null when nothing is found
16
+ * or all files are unparseable.
17
+ *
18
+ * `source` is folded into emitted diagnostics so SAL keeps emitting
19
+ * `sal.eval:...` fingerprints and a future ext-telemetry sink emits
20
+ * `ext.telemetry:...` without changing this loader.
21
+ */
22
+ export function loadInsforgeCredentials(workspaceRoot, source, onDiagnostic, extraEnvPath) {
23
+ const candidates = [
24
+ extraEnvPath,
25
+ join(workspaceRoot, CREDENTIALS_BASENAME, CREDENTIALS_FILE),
26
+ join(homedir(), CREDENTIALS_BASENAME, CREDENTIALS_FILE),
27
+ ].filter((path) => Boolean(path));
28
+ for (const filePath of candidates) {
29
+ const parsed = readFileSafely(filePath, source, onDiagnostic);
30
+ if (!parsed)
31
+ continue;
32
+ const record = pickInsforgeEntry(parsed);
33
+ if (record)
34
+ return record;
35
+ }
36
+ return null;
37
+ }
38
+ function readFileSafely(filePath, source, onDiagnostic) {
39
+ try {
40
+ if (!existsSync(filePath))
41
+ return null;
42
+ const raw = readFileSync(filePath, "utf-8");
43
+ const parsed = JSON.parse(raw);
44
+ if (!parsed || typeof parsed !== "object")
45
+ return null;
46
+ return parsed;
47
+ }
48
+ catch (err) {
49
+ onDiagnostic?.({
50
+ source,
51
+ severity: "warning",
52
+ category: "config",
53
+ message: "Insforge credentials file could not be read.",
54
+ detail: { filePath, error: err.message },
55
+ fingerprint: `${source}:config:credentials-read-failed`,
56
+ });
57
+ return null;
58
+ }
59
+ }
60
+ function pickInsforgeEntry(parsed) {
61
+ // Format 1: { credentials: [{ id: "insforge", ... }, ...] }
62
+ if (Array.isArray(parsed.credentials)) {
63
+ const entry = parsed.credentials.find((e) => e?.id === INSFORGE_ENTRY_ID && e?.enabled !== false);
64
+ if (entry)
65
+ return normalizeEntry(entry);
66
+ return null;
67
+ }
68
+ // Format 2: flat record at top level.
69
+ return normalizeEntry(parsed);
70
+ }
71
+ function normalizeEntry(entry) {
72
+ // Map camelCase aliases → snake_case canonical fields. Preserve any extra
73
+ // (sink-specific) keys verbatim so callers like SAL can read
74
+ // `cleanup_stale_runs` etc. without this loader knowing about them.
75
+ const normalized = {
76
+ ...entry,
77
+ endpoint: entry.endpoint ?? entry.insforge_url,
78
+ insforge_url: entry.insforge_url ?? entry.endpoint,
79
+ api_key: entry.api_key ?? entry.apiKey,
80
+ anon_key: entry.anon_key ?? entry.anonKey,
81
+ api_key_header: entry.api_key_header ?? entry.apiKeyHeader,
82
+ headers: entry.headers,
83
+ allow_self_signed: entry.allow_self_signed ?? entry.allowSelfSigned,
84
+ enabled: entry.enabled,
85
+ };
86
+ return normalized;
87
+ }
@@ -0,0 +1,89 @@
1
+ import type { DiagnosticHandler } from "./types.js";
2
+ /**
3
+ * Sample rates per hook name. Tool-related hooks fire many times per turn
4
+ * (one pair per tool call), so we record only 10% to avoid drowning the
5
+ * table; the sample_rate column lets dashboards extrapolate counts with
6
+ * `count(*) * (1.0 / avg(sample_rate))`. Everything else is rare enough
7
+ * to record in full.
8
+ */
9
+ export declare const HOOK_SAMPLE_RATES: Readonly<Record<string, number>>;
10
+ export type CommandOutcome = "ok" | "error" | "cancelled" | "no_match" | "unknown";
11
+ export interface CommandEventInput {
12
+ extensionName: string;
13
+ commandName: string;
14
+ /** Whitelisted-token-only classification — see classifyArgsSignature(). Never contains original arg text. */
15
+ argsSignature: string;
16
+ argsLength: number;
17
+ outcome: CommandOutcome;
18
+ errorCode?: string | null;
19
+ durationMs: number;
20
+ details?: Record<string, unknown> | null;
21
+ startedAt: Date;
22
+ endedAt: Date;
23
+ runId?: string | null;
24
+ sessionId?: string | null;
25
+ variant?: string | null;
26
+ }
27
+ export interface HookEventInput {
28
+ extensionName: string;
29
+ hookName: string;
30
+ durationMs: number;
31
+ ok: boolean;
32
+ errorCode?: string | null;
33
+ /** Sampling probability used to decide whether to emit. Stored on each row so dashboards can extrapolate counts. */
34
+ sampleRate: number;
35
+ recordedAt: Date;
36
+ runId?: string | null;
37
+ sessionId?: string | null;
38
+ variant?: string | null;
39
+ }
40
+ export interface LlmCallEventInput {
41
+ extensionName: string;
42
+ /** Short scope label, e.g. "command:/recap --smart" or "hook:before_agent_start". */
43
+ callerContext: string;
44
+ /** True for slash-command paths; false for hook auto-fires. SQL dashboards group on this to find idle-thinking-class bugs. */
45
+ isUserInitiated: boolean;
46
+ modelId?: string | null;
47
+ tokensIn?: number | null;
48
+ tokensOut?: number | null;
49
+ costTotal?: number | null;
50
+ durationMs: number;
51
+ ok: boolean;
52
+ errorCode?: string | null;
53
+ startedAt: Date;
54
+ endedAt: Date;
55
+ runId?: string | null;
56
+ sessionId?: string | null;
57
+ variant?: string | null;
58
+ commandEventId?: number | null;
59
+ }
60
+ export interface ExtensionTelemetrySink {
61
+ writeCommandEvent(input: CommandEventInput): void;
62
+ writeLlmCallEvent(input: LlmCallEventInput): void;
63
+ writeHookEvent(input: HookEventInput): void;
64
+ close(): Promise<void>;
65
+ }
66
+ export interface CreateExtensionTelemetrySinkOptions {
67
+ workspaceRoot: string;
68
+ onDiagnostic?: DiagnosticHandler;
69
+ batchIntervalMs?: number;
70
+ }
71
+ /**
72
+ * Build a telemetry sink. Returns a noop sink — silently dropping every event —
73
+ * when insforge credentials are missing or disabled. Callers can therefore wire
74
+ * the sink unconditionally; users without credentials pay zero cost.
75
+ */
76
+ export declare function createExtensionTelemetrySink(options: CreateExtensionTelemetrySinkOptions): ExtensionTelemetrySink;
77
+ /**
78
+ * Classify arg text into a small bounded set of signatures so dashboards can
79
+ * group invocations without ingesting user-supplied text. Privacy posture:
80
+ *
81
+ * - "no-args" — empty or whitespace-only args
82
+ * - "--<flag>" — args start with a `--flag` token; we record the flag name only
83
+ * (enum-like, low cardinality, no PII)
84
+ * - "with-args" — any other non-empty args; original text is never recorded
85
+ *
86
+ * Anything else interesting (length, etc.) lives on its own typed column so we
87
+ * never need free-form arg storage.
88
+ */
89
+ export declare function classifyArgsSignature(args: string): string;