@smeltjs/core 0.5.0 → 0.6.0
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/apply.d.ts.map +1 -1
- package/dist/apply.js +8 -2
- package/dist/apply.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +4 -2
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/report.d.ts +10 -1
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +16 -1
- package/dist/cli/report.js.map +1 -1
- package/dist/cli/subcommands/flags.d.ts +3 -0
- package/dist/cli/subcommands/flags.d.ts.map +1 -1
- package/dist/cli/subcommands/flags.js +16 -3
- package/dist/cli/subcommands/flags.js.map +1 -1
- package/dist/cli/subcommands/smelt.d.ts +3 -0
- package/dist/cli/subcommands/smelt.d.ts.map +1 -1
- package/dist/cli/subcommands/smelt.js +5 -1
- package/dist/cli/subcommands/smelt.js.map +1 -1
- package/dist/cli/subcommands/stats.d.ts +8 -3
- package/dist/cli/subcommands/stats.d.ts.map +1 -1
- package/dist/cli/subcommands/stats.js +15 -5
- package/dist/cli/subcommands/stats.js.map +1 -1
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -1
- package/dist/hooks/focus-terms.d.ts +57 -0
- package/dist/hooks/focus-terms.d.ts.map +1 -0
- package/dist/hooks/focus-terms.js +230 -0
- package/dist/hooks/focus-terms.js.map +1 -0
- package/dist/hooks/guard-core.d.ts +8 -16
- package/dist/hooks/guard-core.d.ts.map +1 -1
- package/dist/hooks/guard-core.js +22 -120
- package/dist/hooks/guard-core.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/ops/index.d.ts +3 -3
- package/dist/ops/index.d.ts.map +1 -1
- package/dist/ops/index.js +2 -2
- package/dist/ops/index.js.map +1 -1
- package/dist/ops/verbs.d.ts +57 -1
- package/dist/ops/verbs.d.ts.map +1 -1
- package/dist/ops/verbs.js +49 -1
- package/dist/ops/verbs.js.map +1 -1
- package/dist/plan/auto.d.ts +9 -2
- package/dist/plan/auto.d.ts.map +1 -1
- package/dist/plan/auto.js +14 -2
- package/dist/plan/auto.js.map +1 -1
- package/dist/plan/diff.d.ts +45 -0
- package/dist/plan/diff.d.ts.map +1 -0
- package/dist/plan/diff.js +284 -0
- package/dist/plan/diff.js.map +1 -0
- package/dist/plan/json.d.ts +37 -0
- package/dist/plan/json.d.ts.map +1 -0
- package/dist/plan/json.js +181 -0
- package/dist/plan/json.js.map +1 -0
- package/dist/plan/kind.d.ts +28 -0
- package/dist/plan/kind.d.ts.map +1 -0
- package/dist/plan/kind.js +57 -0
- package/dist/plan/kind.js.map +1 -0
- package/dist/plan/offsets.d.ts +8 -0
- package/dist/plan/offsets.d.ts.map +1 -0
- package/dist/plan/offsets.js +19 -0
- package/dist/plan/offsets.js.map +1 -0
- package/dist/plan/planners.d.ts +15 -5
- package/dist/plan/planners.d.ts.map +1 -1
- package/dist/plan/planners.js +13 -5
- package/dist/plan/planners.js.map +1 -1
- package/dist/plan/structural.d.ts.map +1 -1
- package/dist/plan/structural.js +106 -14
- package/dist/plan/structural.js.map +1 -1
- package/dist/retrieve.d.ts +22 -1
- package/dist/retrieve.d.ts.map +1 -1
- package/dist/retrieve.js +59 -0
- package/dist/retrieve.js.map +1 -1
- package/dist/smelter.d.ts +4 -0
- package/dist/smelter.d.ts.map +1 -1
- package/dist/smelter.js +4 -0
- package/dist/smelter.js.map +1 -1
- package/dist/stats.d.ts +16 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +29 -0
- package/dist/stats.js.map +1 -1
- package/dist/store-dir.d.ts +9 -2
- package/dist/store-dir.d.ts.map +1 -1
- package/dist/store-dir.js +48 -7
- package/dist/store-dir.js.map +1 -1
- package/dist/store.d.ts +4 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +12 -2
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +116 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard-core.d.ts","sourceRoot":"","sources":["../../src/hooks/guard-core.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,2FAA2F;AAC3F,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,OAAO,CAAC;AAEpD,yFAAyF;AACzF,eAAO,MAAM,iBAAiB,YAAI,MAAM,EAAE,SAAS,CAAU,CAAC;AAC9D,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE,2EAA2E;AAC3E,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAE1D;;;;;;;;;GASG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAQxC;AAID,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE;QACd,mFAAmF;QACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QACvB,0DAA0D;QAC1D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAC1B;;;WAGG;QACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;KAClC,CAAC;CACH;AAED,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;IAClC,uFAAuF;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,eAAe,CAAC;IACtC,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAED,eAAO,MAAM,sBAAsB,EAAE,aAKpC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,aAAa,CA2C1F;AAuCD,0FAA0F;AAC1F,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASnE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CA2BxE;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,MAAM,CACpB,OAAO,EAAE,YAAY,EACrB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG,SAAwB,GACvF,aAAa,CAQf;AAmID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,CAuCjF;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAiD1E;AAED,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGhD;AAMD,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAQvD;AAED,qFAAqF;AACrF,wBAAgB,cAAc,IAAI,MAAM,CAqBvC","sourcesContent":["import { readSync, statSync, existsSync, readFileSync } from 'node:fs';\nimport { dirname, isAbsolute, join, resolve } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport process from 'node:process';\n\n/**\n * The guard core — one zero-dependency node module, shared by every harness shim.\n *\n * Contract: a caller hands `decide` a {@link GuardRequest}\n * (`{ tool, input: { path?, command?, offsetLimited? } }`) with the settings\n * {@link readGuardSettings} read, and gets one {@link GuardDecision} back —\n * `{ action: \"allow\" | \"deny\", reason?, suggestion? }`. Each shim translates that into\n * its harness's own schema (exit 2, `permissionDecision`, `cancel:true`, …), and the\n * opencode plugin — the one harness whose hook API is JavaScript — imports this module\n * at hook time and calls the same two functions.\n *\n * Two properties are load-bearing and guarded:\n *\n * - **Fail open, loudly.** Malformed stdin, a malformed config, an unstatable path —\n * every degenerate input produces `{\"action\":\"allow\"}` plus a warning on stderr.\n * A guard that can brick a session on bad input is worse than no guard; the agent\n * loses nothing but the optimization, and the warning says so.\n * - **No library import on any path.** This module imports node builtins only —\n * never `../index.ts`, never a planner, never web-tree-sitter. The allow case is\n * a stat and an exit; the research note\n * (docs/research/2026-09-02-agent-enforcement.md § 5) budgets the always-on guard\n * at tens of milliseconds, and loading grammar machinery here would spend that\n * budget before deciding anything. The smelt run itself is only ever paid by the\n * *replacement* command the model runs after a deny (or the rewritten command).\n *\n * Config: the nearest `smelt.config.json` (walking up from the cwd, same discovery\n * as the CLI) may carry a `hooks` block — `thresholdBytes` and `enforcement` — plus\n * the `defaultBudgetBytes` the suggested command quotes. This file reads that config\n * with its own tolerant reader instead of importing `cli/config.ts`: the CLI's strict\n * parser sits on the planner import graph, and a *guard* must fail open where the CLI\n * correctly refuses. `test/hooks-guard-core.test.ts` pins the two readers to the same\n * key names and defaults, so they cannot drift apart silently.\n */\n\n/** Deny threshold when no config says otherwise: reads at or under this pass untouched. */\nexport const DEFAULT_THRESHOLD_BYTES = 8192;\n\n/**\n * The `--budget` the suggested replacement command quotes when no config carries\n * `defaultBudgetBytes`. A suggestion default, not a smelt default: the CLI itself\n * still refuses to run without an explicit budget from a flag or the config.\n */\nexport const DEFAULT_SUGGESTION_BUDGET_BYTES = 8000;\n\n/** The `hooks.enforcement` values `smelt.config.json` may carry. Deny is the default. */\nexport const ENFORCEMENT_MODES = ['deny', 'rewrite'] as const;\nexport type EnforcementMode = (typeof ENFORCEMENT_MODES)[number];\n\n/** The config file this guard discovers, by the same name the CLI uses. */\nexport const GUARD_CONFIG_FILE_NAME = 'smelt.config.json';\n\n/**\n * The runnable CLI name every reason and suggestion quotes. A local (non-global)\n * `npm install @smeltjs/core` puts no `smelt` on anyone's PATH — the installer wires\n * every shim as `node \"<dist path>\"` for exactly that reason — so a suggestion\n * saying bare `smelt` would exit 127 the moment the model (or a rewrite-mode\n * harness) ran it. When this module's sibling `cli/bin.js` exists — the shipped\n * `dist/` layout every real run executes from — the command names it through `node`\n * explicitly; the bare name is only the fallback for layouts where the sibling is\n * absent (the source tree under the test runner).\n */\nexport function smeltCliCommand(): string {\n try {\n const bin = join(dirname(fileURLToPath(import.meta.url)), '..', 'cli', 'bin.js');\n if (existsSync(bin)) return `node ${shellQuote(bin)}`;\n } catch {\n // fall through to the PATH name\n }\n return 'smelt';\n}\n\nconst SMELT_CLI = smeltCliCommand();\n\n/** What a shim hands the guard core: the harness schema already mapped away. */\nexport interface GuardRequest {\n /** `'Read'` for a file-read tool, `'Bash'` for a shell tool; anything else passes. */\n readonly tool: string;\n readonly input: {\n /** The file a Read-shaped tool targets. Relative paths resolve against the cwd. */\n readonly path?: string;\n /** The command a Bash-shaped tool would run, verbatim. */\n readonly command?: string;\n /**\n * True when the read is already windowed (offset/limit given). A windowed read\n * of a huge file is an economy move — it is always allowed, whatever the size.\n */\n readonly offsetLimited?: boolean;\n };\n}\n\n/** The guard's whole answer. `suggestion`, when present, is an executable command. */\nexport interface GuardDecision {\n readonly action: 'allow' | 'deny';\n /** Why, written to steer: names the exact replacement command and `smelt retrieve`. */\n readonly reason?: string;\n /**\n * A command that faithfully replaces the denied one — `smelt <path> --budget <n>`\n * for a raw read, the original pipeline with ` | smelt …` appended for a search.\n * Only emitted when running it preserves the intent of the original call, which is\n * exactly the condition under which a rewrite-mode shim may substitute it via\n * `updatedInput`. Absent on decisions that need the model's judgement instead.\n */\n readonly suggestion?: string;\n}\n\n/** The guard's merged settings: config values where sane, defaults where not. */\nexport interface GuardSettings {\n readonly thresholdBytes: number;\n readonly enforcement: EnforcementMode;\n /** Quoted in every suggested command, so the model runs a complete line. */\n readonly budgetBytes: number;\n /**\n * True when the config carries a directory store. `smelt retrieve <hash>` only\n * works across processes with a persistent store (the CLI's default is memory,\n * which dies with the process that elided), so a deny reason may only *promise*\n * retrieval when this is true — otherwise it says what to configure instead.\n */\n readonly persistentStore: boolean;\n}\n\nexport const DEFAULT_GUARD_SETTINGS: GuardSettings = {\n thresholdBytes: DEFAULT_THRESHOLD_BYTES,\n enforcement: 'deny',\n budgetBytes: DEFAULT_SUGGESTION_BUDGET_BYTES,\n persistentStore: false,\n};\n\n/**\n * Read the nearest `smelt.config.json`'s guard-relevant fields, tolerantly.\n *\n * Tolerant is a deliberate divergence from the CLI: `smelt` refuses a malformed\n * config because a silently skipped setting is a setting the user believed was in\n * force — but this code runs inside somebody's *session*, before every Read, and a\n * guard that turns a config typo into a hard-down harness has failed worse than the\n * typo. So: any unreadable or ill-typed field falls back to its default, and `warn`\n * receives one line saying which file and why — visible in the harness's hook debug\n * output, never fatal.\n */\nexport function readGuardSettings(cwd: string, warn: (text: string) => void): GuardSettings {\n const path = findGuardConfigFile(cwd);\n if (path === undefined) return DEFAULT_GUARD_SETTINGS;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(path, 'utf8'));\n } catch (cause) {\n warn(\n `smelt guard: ${path} is not readable JSON ` +\n `(${cause instanceof Error ? cause.message : String(cause)}) — ` +\n `guarding with defaults instead. \\`smelt\\` itself will refuse this config.`,\n );\n return DEFAULT_GUARD_SETTINGS;\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n warn(`smelt guard: ${path} is not a JSON object — guarding with defaults instead.`);\n return DEFAULT_GUARD_SETTINGS;\n }\n const fields = parsed as Record<string, unknown>;\n const hooks =\n typeof fields['hooks'] === 'object' &&\n fields['hooks'] !== null &&\n !Array.isArray(fields['hooks'])\n ? (fields['hooks'] as Record<string, unknown>)\n : {};\n\n return {\n thresholdBytes: positiveInteger(\n hooks['thresholdBytes'],\n DEFAULT_THRESHOLD_BYTES,\n `${path}: hooks.thresholdBytes`,\n warn,\n ),\n enforcement: enforcementMode(hooks['enforcement'], `${path}: hooks.enforcement`, warn),\n budgetBytes: positiveInteger(\n fields['defaultBudgetBytes'],\n DEFAULT_SUGGESTION_BUDGET_BYTES,\n `${path}: defaultBudgetBytes`,\n warn,\n ),\n persistentStore: isDirectoryStore(fields['store']),\n };\n}\n\n/** True for a well-formed `{\"kind\":\"directory\",\"path\":…}` store block; no warning\n * otherwise — an absent or memory store is a valid (just non-persistent) choice. */\nfunction isDirectoryStore(value: unknown): boolean {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const fields = value as Record<string, unknown>;\n return fields['kind'] === 'directory' && typeof fields['path'] === 'string';\n}\n\nfunction positiveInteger(\n value: unknown,\n fallback: number,\n what: string,\n warn: (text: string) => void,\n): number {\n if (value === undefined) return fallback;\n if (typeof value === 'number' && Number.isInteger(value) && value > 0) return value;\n warn(\n `smelt guard: ${what} must be a positive integer, got ${JSON.stringify(value)} — ` +\n `using ${String(fallback)}.`,\n );\n return fallback;\n}\n\nfunction enforcementMode(\n value: unknown,\n what: string,\n warn: (text: string) => void,\n): EnforcementMode {\n if (value === undefined) return 'deny';\n if (value === 'deny' || value === 'rewrite') return value;\n warn(\n `smelt guard: ${what} must be \"deny\" or \"rewrite\", got ${JSON.stringify(value)} — ` +\n `using \"deny\".`,\n );\n return 'deny';\n}\n\n/** The same upward walk `cli/config.ts` does, re-implemented to keep this module tiny. */\nexport function findGuardConfigFile(cwd: string): string | undefined {\n let dir = resolve(cwd);\n for (;;) {\n const candidate = join(dir, GUARD_CONFIG_FILE_NAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n/**\n * Parse one {@link GuardRequest} *document* — the request shape as JSON, for an\n * adapter that receives it over a pipe rather than building it in-process.\n * `undefined` means malformed, and a caller that gets it allows and warns: the same\n * fail-open rule the rest of this module lives under.\n */\nexport function parseGuardRequest(text: string): GuardRequest | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return undefined;\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return undefined;\n const fields = parsed as Record<string, unknown>;\n if (typeof fields['tool'] !== 'string') return undefined;\n const input = fields['input'];\n if (typeof input !== 'object' || input === null || Array.isArray(input)) return undefined;\n const inputFields = input as Record<string, unknown>;\n const path = inputFields['path'];\n const command = inputFields['command'];\n const offsetLimited = inputFields['offsetLimited'];\n if (path !== undefined && typeof path !== 'string') return undefined;\n if (command !== undefined && typeof command !== 'string') return undefined;\n if (offsetLimited !== undefined && typeof offsetLimited !== 'boolean') return undefined;\n return {\n tool: fields['tool'],\n input: {\n ...(path === undefined ? {} : { path }),\n ...(command === undefined ? {} : { command }),\n ...(offsetLimited === undefined ? {} : { offsetLimited }),\n },\n };\n}\n\nconst ALLOW: GuardDecision = { action: 'allow' };\n\n/**\n * The decision, pure given a stat function — so tests exercise every branch without\n * a filesystem, and the script wires `statSync` in.\n *\n * The shape of the rules, from the research note (§ 5, \"the ~8 KB threshold,\n * validated\", with both amendments):\n *\n * - **Read**: stat the exact target; deny only above the threshold, and never when\n * the read is already windowed (`offsetLimited`) — a windowed read of a huge file\n * is an economy move.\n * - **Bash**: only a *simple* command whose subject is a statable named file can be\n * judged pre-run. `cat <file>` above the threshold is denied with the faithful\n * replacement; `grep`/`rg` output is unknowable pre-run, so it passes in deny\n * mode and is wrapped (` | smelt --budget … --focus <pattern>`) only under\n * `hooks.enforcement: \"rewrite\"`. Pipelines, redirects, substitutions — anything\n * this parser cannot be sure about — pass untouched. Fail open, always.\n */\nexport function decide(\n request: GuardRequest,\n settings: GuardSettings,\n cwd: string,\n statFile: (path: string) => { size: number; isFile: boolean } | undefined = statFileReal,\n): GuardDecision {\n if (request.tool === 'Read') {\n return decideRead(request.input, settings, cwd, statFile);\n }\n if (request.tool === 'Bash') {\n return decideBash(request.input, settings, cwd, statFile);\n }\n return ALLOW;\n}\n\nfunction statFileReal(path: string): { size: number; isFile: boolean } | undefined {\n try {\n const stat = statSync(path);\n return { size: stat.size, isFile: stat.isFile() };\n } catch {\n return undefined;\n }\n}\n\nfunction decideRead(\n input: GuardRequest['input'],\n settings: GuardSettings,\n cwd: string,\n statFile: (path: string) => { size: number; isFile: boolean } | undefined,\n): GuardDecision {\n if (input.path === undefined) return ALLOW;\n if (input.offsetLimited === true) return ALLOW; // already windowed — an economy move\n const path = isAbsolute(input.path) ? input.path : resolve(cwd, input.path);\n const stat = statFile(path);\n if (stat === undefined || !stat.isFile) return ALLOW; // let the tool surface its own error\n if (stat.size <= settings.thresholdBytes) return ALLOW;\n return denyOversized(path, stat.size, settings, 'Reading it raw');\n}\n\nfunction decideBash(\n input: GuardRequest['input'],\n settings: GuardSettings,\n cwd: string,\n statFile: (path: string) => { size: number; isFile: boolean } | undefined,\n): GuardDecision {\n if (input.command === undefined) return ALLOW;\n const command = input.command.trim();\n // A command already using smelt is the model doing the right thing — including the\n // exact replacement a previous deny suggested. Never intercept it (and never wrap a\n // wrapped pipeline a second time).\n if (/(^|[\\s/\"'=])smelt($|[\\s\"'])/.test(command)) return ALLOW;\n\n const words = simpleCommandWords(command);\n if (words === undefined || words.length === 0) return ALLOW; // not simple — unknowable pre-run\n\n const program = words[0]!.split('/').at(-1)!;\n\n if (program === 'cat') {\n const files = words.slice(1).filter((word) => word !== '--' && !word.startsWith('-'));\n for (const file of files) {\n const path = isAbsolute(file) ? file : resolve(cwd, file);\n const stat = statFile(path);\n if (stat === undefined || !stat.isFile) continue;\n if (stat.size > settings.thresholdBytes) {\n const decision = denyOversized(path, stat.size, settings, `\\`${command}\\``);\n // The suggestion is only a *faithful* replacement when cat named exactly this\n // one file; `cat a b` replaced by `smelt a` would silently drop b, so the\n // multi-file case keeps the reason (the model decides), drops the suggestion\n // (nothing may auto-substitute it), and says out loud that the named\n // replacement covers only the oversized file — a model following the reason\n // verbatim must not silently drop the others.\n return files.length === 1\n ? decision\n : {\n action: 'deny',\n reason:\n `${decision.reason ?? ''} Note: \\`${command}\\` names ` +\n `${String(files.length)} files and the replacement above covers only ` +\n `${path} — read the other file(s) separately (cat is fine for the ones ` +\n `under the threshold).`,\n };\n }\n }\n return ALLOW;\n }\n\n if ((program === 'grep' || program === 'rg') && settings.enforcement === 'rewrite') {\n const pattern = searchPattern(words);\n if (pattern === undefined) return ALLOW;\n // Deliberately no `--focus` on the wrap: a plain grep's every output line contains\n // the searched pattern, so focusing on it would protect the entire output — zero\n // elisions exactly when the output is large, plus an over-budget exit. The wrap\n // lets smelt's lexical planner keep the head and tail and collapse the middle\n // into retrievable markers instead.\n const wrapped = `${command} | ${SMELT_CLI} --budget ${String(settings.budgetBytes)}`;\n return {\n action: 'deny',\n reason:\n `smelt guard (rewrite mode): \\`${program}\\` output size is unknowable before it runs, ` +\n `so pipe it through smelt instead. Run exactly: ${wrapped} — output within the ` +\n `budget passes through untouched; past it, elided regions leave <<smelt/v1 …>> ` +\n `markers. ${retrieveSentence(settings)}`,\n suggestion: wrapped,\n };\n }\n\n return ALLOW;\n}\n\n/**\n * The one sentence about getting elided bytes back — honest about the store: the\n * retrieval promise is only made when a persistent store is configured, because a\n * memory store dies with the process and `retrieve` then refuses (`resolveStoreRun`).\n */\nfunction retrieveSentence(settings: GuardSettings): string {\n return settings.persistentStore\n ? `\\`${SMELT_CLI} retrieve <hash>\\` prints any marker's bytes back, byte for byte.`\n : `\\`${SMELT_CLI} retrieve <hash>\\` can print a marker's bytes back once a persistent ` +\n `store is configured ({\"store\":{\"kind\":\"directory\",\"path\":…}} in smelt.config.json — ` +\n `\\`smelt hooks install\\` writes one); without it the elided bytes die with the ` +\n `smelt process.`;\n}\n\n/** The deny everything above the threshold gets: steering text plus the exact command. */\nfunction denyOversized(\n path: string,\n size: number,\n settings: GuardSettings,\n what: string,\n): GuardDecision {\n const replacement = `${SMELT_CLI} ${shellQuote(path)} --budget ${String(settings.budgetBytes)}`;\n return {\n action: 'deny',\n reason:\n `smelt guard: ${path} is ${String(size)} bytes — over the ${String(settings.thresholdBytes)}-byte ` +\n `threshold (smelt.config.json hooks.thresholdBytes). ${what} would spend context on bytes ` +\n `the task may not need. Run instead: ${replacement} --focus <what you are looking for> ` +\n `(repeat --focus per term; focused regions survive verbatim). Elided regions leave ` +\n `<<smelt/v1 …>> markers — ${retrieveSentence(settings)} A windowed read (offset/limit) ` +\n `of just the lines you need is also fine.`,\n suggestion: replacement,\n };\n}\n\n/**\n * Split a command into words IF it is one simple command: no pipes, no logic, no\n * redirects, no substitutions, no expansions this code would have to model. Anything\n * else returns `undefined` and the caller allows — the guard judges only what it can\n * see whole.\n */\nexport function simpleCommandWords(command: string): readonly string[] | undefined {\n const words: string[] = [];\n let current = '';\n let started = false;\n let i = 0;\n const push = (): void => {\n if (started) words.push(current);\n current = '';\n started = false;\n };\n while (i < command.length) {\n const ch = command[i]!;\n if ('|&;<>()`$\\\\\\n*?~{}!'.includes(ch)) return undefined; // shell would interpret it\n if (ch === \"'\" || ch === '\"') {\n const quote = ch;\n i += 1;\n started = true;\n while (i < command.length && command[i] !== quote) {\n if (quote === '\"' && (command[i] === '$' || command[i] === '`' || command[i] === '\\\\')) {\n return undefined; // expansions inside double quotes — not simple\n }\n current += command[i]!;\n i += 1;\n }\n if (i >= command.length) return undefined; // unterminated quote\n i += 1;\n continue;\n }\n if (ch === ' ' || ch === '\\t') {\n push();\n i += 1;\n continue;\n }\n current += ch;\n started = true;\n i += 1;\n }\n push();\n return words;\n}\n\n/**\n * The pattern a grep/rg invocation searches for: an explicit `-e`/`--regexp` value if\n * given, else the first word that is not a flag or a flag's value. `undefined` when\n * the parse is not sure — and unsure means allow, like everything else here.\n */\nexport function searchPattern(words: readonly string[]): string | undefined {\n const takesValue = new Set([\n '-e',\n '--regexp',\n '-f',\n '--file',\n '-m',\n '--max-count',\n '-A',\n '--after-context',\n '-B',\n '--before-context',\n '-C',\n '--context',\n '-d',\n '--directories',\n '-D',\n '--devices',\n '--include',\n '--exclude',\n '--exclude-dir',\n '-t',\n '--type',\n '-T',\n '--type-not',\n '-g',\n '--glob',\n '--iglob',\n '-j',\n '--threads',\n '--color',\n '--colour',\n ]);\n let i = 1;\n while (i < words.length) {\n const word = words[i]!;\n if (word === '--') return words[i + 1];\n if (word === '-e' || word === '--regexp') return words[i + 1];\n if (word.startsWith('--') && word.includes('=')) {\n i += 1;\n continue;\n }\n if (word.startsWith('-') && word.length > 1) {\n i += takesValue.has(word) ? 2 : 1;\n continue;\n }\n return word;\n }\n return undefined;\n}\n\n/** Single-quote a value for `sh` unless it is plainly safe bare. */\nexport function shellQuote(value: string): string {\n if (/^[A-Za-z0-9_./:=-]+$/.test(value)) return value;\n return `'${value.replaceAll(\"'\", `'\"'\"'`)}'`;\n}\n\n/* ------------------------------------------------------------------------------------\n * Process plumbing, for the shims that run as one\n * ---------------------------------------------------------------------------------- */\n\n/** True when this module is the file node was asked to run, not an import. */\nexport function isMainModule(moduleUrl: string): boolean {\n const entry = process.argv[1];\n if (entry === undefined) return false;\n try {\n return pathToFileURL(entry).href === moduleUrl;\n } catch {\n return false;\n }\n}\n\n/** Every byte of fd 0 to EOF, retrying EAGAIN — the same shape `cli/bin.ts` uses. */\nexport function readAllOfStdin(): string {\n const sleeper = new Int32Array(new SharedArrayBuffer(4));\n const chunks: Buffer[] = [];\n const chunk = Buffer.alloc(1 << 16);\n for (;;) {\n let bytesRead: number;\n try {\n bytesRead = readSync(0, chunk, 0, chunk.length, null);\n } catch (error) {\n const code = (error as { code?: string }).code;\n if (code === 'EAGAIN') {\n Atomics.wait(sleeper, 0, 0, 10);\n continue;\n }\n if (code === 'EOF') break;\n throw error;\n }\n if (bytesRead === 0) break;\n chunks.push(Buffer.from(chunk.subarray(0, bytesRead)));\n }\n return Buffer.concat(chunks).toString('utf8');\n}\n"]}
|
|
1
|
+
{"version":3,"file":"guard-core.d.ts","sourceRoot":"","sources":["../../src/hooks/guard-core.ts"],"names":[],"mappings":"AAOA;;;;GAIG;AACH,OAAO,EACL,aAAa,EACb,aAAa,EACb,cAAc,EACd,UAAU,EACV,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAE1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,2FAA2F;AAC3F,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,OAAO,CAAC;AAEpD,yFAAyF;AACzF,eAAO,MAAM,iBAAiB,YAAI,MAAM,EAAE,SAAS,CAAU,CAAC;AAC9D,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE,2EAA2E;AAC3E,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAE1D;;;;;;;;;GASG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAQxC;AAID,gFAAgF;AAChF,MAAM,WAAW,YAAY;IAC3B,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE;QACd,mFAAmF;QACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QACvB,0DAA0D;QAC1D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAC1B;;;WAGG;QACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;KAClC,CAAC;CACH;AAED,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC;IAClC,uFAAuF;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,eAAe,CAAC;IACtC,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CACnC;AAED,eAAO,MAAM,sBAAsB,EAAE,aAKpC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,aAAa,CA2C1F;AAuCD,0FAA0F;AAC1F,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CASnE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CA2BxE;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,MAAM,CACpB,OAAO,EAAE,YAAY,EACrB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG,SAAwB,GACvF,aAAa,CAQf;AA6ID,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAQvD;AAED,qFAAqF;AACrF,wBAAgB,cAAc,IAAI,MAAM,CAqBvC","sourcesContent":["import { readSync, statSync, existsSync, readFileSync } from 'node:fs';\nimport { dirname, isAbsolute, join, resolve } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport process from 'node:process';\n\nimport { focusTermsFor, searchPattern, shellQuote, simpleCommandWords } from './focus-terms.ts';\n\n/**\n * The command parsing lives in `./focus-terms.ts` — a zero-import sibling, so the\n * guard's no-library-import rule holds — and is re-exported here because this module\n * is the published `hooks/guard-core` subpath every shim and the opencode plugin load.\n */\nexport {\n focusTermsFor,\n searchPattern,\n searchPatterns,\n shellQuote,\n simpleCommandWords,\n} from './focus-terms.ts';\n\n/**\n * The guard core — one zero-dependency node module, shared by every harness shim.\n *\n * Contract: a caller hands `decide` a {@link GuardRequest}\n * (`{ tool, input: { path?, command?, offsetLimited? } }`) with the settings\n * {@link readGuardSettings} read, and gets one {@link GuardDecision} back —\n * `{ action: \"allow\" | \"deny\", reason?, suggestion? }`. Each shim translates that into\n * its harness's own schema (exit 2, `permissionDecision`, `cancel:true`, …), and the\n * opencode plugin — the one harness whose hook API is JavaScript — imports this module\n * at hook time and calls the same two functions.\n *\n * Two properties are load-bearing and guarded:\n *\n * - **Fail open, loudly.** Malformed stdin, a malformed config, an unstatable path —\n * every degenerate input produces `{\"action\":\"allow\"}` plus a warning on stderr.\n * A guard that can brick a session on bad input is worse than no guard; the agent\n * loses nothing but the optimization, and the warning says so.\n * - **No library import on any path.** This module imports node builtins only —\n * never `../index.ts`, never a planner, never web-tree-sitter — plus its one\n * zero-import sibling `./focus-terms.ts`, which owns the command parsing. The allow case is\n * a stat and an exit; the research note\n * (docs/research/2026-09-02-agent-enforcement.md § 5) budgets the always-on guard\n * at tens of milliseconds, and loading grammar machinery here would spend that\n * budget before deciding anything. The smelt run itself is only ever paid by the\n * *replacement* command the model runs after a deny (or the rewritten command).\n *\n * Config: the nearest `smelt.config.json` (walking up from the cwd, same discovery\n * as the CLI) may carry a `hooks` block — `thresholdBytes` and `enforcement` — plus\n * the `defaultBudgetBytes` the suggested command quotes. This file reads that config\n * with its own tolerant reader instead of importing `cli/config.ts`: the CLI's strict\n * parser sits on the planner import graph, and a *guard* must fail open where the CLI\n * correctly refuses. `test/hooks-guard-core.test.ts` pins the two readers to the same\n * key names and defaults, so they cannot drift apart silently.\n */\n\n/** Deny threshold when no config says otherwise: reads at or under this pass untouched. */\nexport const DEFAULT_THRESHOLD_BYTES = 8192;\n\n/**\n * The `--budget` the suggested replacement command quotes when no config carries\n * `defaultBudgetBytes`. A suggestion default, not a smelt default: the CLI itself\n * still refuses to run without an explicit budget from a flag or the config.\n */\nexport const DEFAULT_SUGGESTION_BUDGET_BYTES = 8000;\n\n/** The `hooks.enforcement` values `smelt.config.json` may carry. Deny is the default. */\nexport const ENFORCEMENT_MODES = ['deny', 'rewrite'] as const;\nexport type EnforcementMode = (typeof ENFORCEMENT_MODES)[number];\n\n/** The config file this guard discovers, by the same name the CLI uses. */\nexport const GUARD_CONFIG_FILE_NAME = 'smelt.config.json';\n\n/**\n * The runnable CLI name every reason and suggestion quotes. A local (non-global)\n * `npm install @smeltjs/core` puts no `smelt` on anyone's PATH — the installer wires\n * every shim as `node \"<dist path>\"` for exactly that reason — so a suggestion\n * saying bare `smelt` would exit 127 the moment the model (or a rewrite-mode\n * harness) ran it. When this module's sibling `cli/bin.js` exists — the shipped\n * `dist/` layout every real run executes from — the command names it through `node`\n * explicitly; the bare name is only the fallback for layouts where the sibling is\n * absent (the source tree under the test runner).\n */\nexport function smeltCliCommand(): string {\n try {\n const bin = join(dirname(fileURLToPath(import.meta.url)), '..', 'cli', 'bin.js');\n if (existsSync(bin)) return `node ${shellQuote(bin)}`;\n } catch {\n // fall through to the PATH name\n }\n return 'smelt';\n}\n\nconst SMELT_CLI = smeltCliCommand();\n\n/** What a shim hands the guard core: the harness schema already mapped away. */\nexport interface GuardRequest {\n /** `'Read'` for a file-read tool, `'Bash'` for a shell tool; anything else passes. */\n readonly tool: string;\n readonly input: {\n /** The file a Read-shaped tool targets. Relative paths resolve against the cwd. */\n readonly path?: string;\n /** The command a Bash-shaped tool would run, verbatim. */\n readonly command?: string;\n /**\n * True when the read is already windowed (offset/limit given). A windowed read\n * of a huge file is an economy move — it is always allowed, whatever the size.\n */\n readonly offsetLimited?: boolean;\n };\n}\n\n/** The guard's whole answer. `suggestion`, when present, is an executable command. */\nexport interface GuardDecision {\n readonly action: 'allow' | 'deny';\n /** Why, written to steer: names the exact replacement command and `smelt retrieve`. */\n readonly reason?: string;\n /**\n * A command that faithfully replaces the denied one — `smelt <path> --budget <n>`\n * for a raw read, the original pipeline with ` | smelt …` appended for a search.\n * Only emitted when running it preserves the intent of the original call, which is\n * exactly the condition under which a rewrite-mode shim may substitute it via\n * `updatedInput`. Absent on decisions that need the model's judgement instead.\n */\n readonly suggestion?: string;\n}\n\n/** The guard's merged settings: config values where sane, defaults where not. */\nexport interface GuardSettings {\n readonly thresholdBytes: number;\n readonly enforcement: EnforcementMode;\n /** Quoted in every suggested command, so the model runs a complete line. */\n readonly budgetBytes: number;\n /**\n * True when the config carries a directory store. `smelt retrieve <hash>` only\n * works across processes with a persistent store (the CLI's default is memory,\n * which dies with the process that elided), so a deny reason may only *promise*\n * retrieval when this is true — otherwise it says what to configure instead.\n */\n readonly persistentStore: boolean;\n}\n\nexport const DEFAULT_GUARD_SETTINGS: GuardSettings = {\n thresholdBytes: DEFAULT_THRESHOLD_BYTES,\n enforcement: 'deny',\n budgetBytes: DEFAULT_SUGGESTION_BUDGET_BYTES,\n persistentStore: false,\n};\n\n/**\n * Read the nearest `smelt.config.json`'s guard-relevant fields, tolerantly.\n *\n * Tolerant is a deliberate divergence from the CLI: `smelt` refuses a malformed\n * config because a silently skipped setting is a setting the user believed was in\n * force — but this code runs inside somebody's *session*, before every Read, and a\n * guard that turns a config typo into a hard-down harness has failed worse than the\n * typo. So: any unreadable or ill-typed field falls back to its default, and `warn`\n * receives one line saying which file and why — visible in the harness's hook debug\n * output, never fatal.\n */\nexport function readGuardSettings(cwd: string, warn: (text: string) => void): GuardSettings {\n const path = findGuardConfigFile(cwd);\n if (path === undefined) return DEFAULT_GUARD_SETTINGS;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(path, 'utf8'));\n } catch (cause) {\n warn(\n `smelt guard: ${path} is not readable JSON ` +\n `(${cause instanceof Error ? cause.message : String(cause)}) — ` +\n `guarding with defaults instead. \\`smelt\\` itself will refuse this config.`,\n );\n return DEFAULT_GUARD_SETTINGS;\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n warn(`smelt guard: ${path} is not a JSON object — guarding with defaults instead.`);\n return DEFAULT_GUARD_SETTINGS;\n }\n const fields = parsed as Record<string, unknown>;\n const hooks =\n typeof fields['hooks'] === 'object' &&\n fields['hooks'] !== null &&\n !Array.isArray(fields['hooks'])\n ? (fields['hooks'] as Record<string, unknown>)\n : {};\n\n return {\n thresholdBytes: positiveInteger(\n hooks['thresholdBytes'],\n DEFAULT_THRESHOLD_BYTES,\n `${path}: hooks.thresholdBytes`,\n warn,\n ),\n enforcement: enforcementMode(hooks['enforcement'], `${path}: hooks.enforcement`, warn),\n budgetBytes: positiveInteger(\n fields['defaultBudgetBytes'],\n DEFAULT_SUGGESTION_BUDGET_BYTES,\n `${path}: defaultBudgetBytes`,\n warn,\n ),\n persistentStore: isDirectoryStore(fields['store']),\n };\n}\n\n/** True for a well-formed `{\"kind\":\"directory\",\"path\":…}` store block; no warning\n * otherwise — an absent or memory store is a valid (just non-persistent) choice. */\nfunction isDirectoryStore(value: unknown): boolean {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const fields = value as Record<string, unknown>;\n return fields['kind'] === 'directory' && typeof fields['path'] === 'string';\n}\n\nfunction positiveInteger(\n value: unknown,\n fallback: number,\n what: string,\n warn: (text: string) => void,\n): number {\n if (value === undefined) return fallback;\n if (typeof value === 'number' && Number.isInteger(value) && value > 0) return value;\n warn(\n `smelt guard: ${what} must be a positive integer, got ${JSON.stringify(value)} — ` +\n `using ${String(fallback)}.`,\n );\n return fallback;\n}\n\nfunction enforcementMode(\n value: unknown,\n what: string,\n warn: (text: string) => void,\n): EnforcementMode {\n if (value === undefined) return 'deny';\n if (value === 'deny' || value === 'rewrite') return value;\n warn(\n `smelt guard: ${what} must be \"deny\" or \"rewrite\", got ${JSON.stringify(value)} — ` +\n `using \"deny\".`,\n );\n return 'deny';\n}\n\n/** The same upward walk `cli/config.ts` does, re-implemented to keep this module tiny. */\nexport function findGuardConfigFile(cwd: string): string | undefined {\n let dir = resolve(cwd);\n for (;;) {\n const candidate = join(dir, GUARD_CONFIG_FILE_NAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n/**\n * Parse one {@link GuardRequest} *document* — the request shape as JSON, for an\n * adapter that receives it over a pipe rather than building it in-process.\n * `undefined` means malformed, and a caller that gets it allows and warns: the same\n * fail-open rule the rest of this module lives under.\n */\nexport function parseGuardRequest(text: string): GuardRequest | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return undefined;\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return undefined;\n const fields = parsed as Record<string, unknown>;\n if (typeof fields['tool'] !== 'string') return undefined;\n const input = fields['input'];\n if (typeof input !== 'object' || input === null || Array.isArray(input)) return undefined;\n const inputFields = input as Record<string, unknown>;\n const path = inputFields['path'];\n const command = inputFields['command'];\n const offsetLimited = inputFields['offsetLimited'];\n if (path !== undefined && typeof path !== 'string') return undefined;\n if (command !== undefined && typeof command !== 'string') return undefined;\n if (offsetLimited !== undefined && typeof offsetLimited !== 'boolean') return undefined;\n return {\n tool: fields['tool'],\n input: {\n ...(path === undefined ? {} : { path }),\n ...(command === undefined ? {} : { command }),\n ...(offsetLimited === undefined ? {} : { offsetLimited }),\n },\n };\n}\n\nconst ALLOW: GuardDecision = { action: 'allow' };\n\n/**\n * The decision, pure given a stat function — so tests exercise every branch without\n * a filesystem, and the script wires `statSync` in.\n *\n * The shape of the rules, from the research note (§ 5, \"the ~8 KB threshold,\n * validated\", with both amendments):\n *\n * - **Read**: stat the exact target; deny only above the threshold, and never when\n * the read is already windowed (`offsetLimited`) — a windowed read of a huge file\n * is an economy move.\n * - **Bash**: only a *simple* command whose subject is a statable named file can be\n * judged pre-run. `cat <file>` above the threshold is denied with the faithful\n * replacement; `grep`/`rg` output is unknowable pre-run, so it passes in deny\n * mode and is wrapped (` | smelt --budget … --focus <pattern>`) only under\n * `hooks.enforcement: \"rewrite\"`. Pipelines, redirects, substitutions — anything\n * this parser cannot be sure about — pass untouched. Fail open, always.\n */\nexport function decide(\n request: GuardRequest,\n settings: GuardSettings,\n cwd: string,\n statFile: (path: string) => { size: number; isFile: boolean } | undefined = statFileReal,\n): GuardDecision {\n if (request.tool === 'Read') {\n return decideRead(request.input, settings, cwd, statFile);\n }\n if (request.tool === 'Bash') {\n return decideBash(request.input, settings, cwd, statFile);\n }\n return ALLOW;\n}\n\nfunction statFileReal(path: string): { size: number; isFile: boolean } | undefined {\n try {\n const stat = statSync(path);\n return { size: stat.size, isFile: stat.isFile() };\n } catch {\n return undefined;\n }\n}\n\nfunction decideRead(\n input: GuardRequest['input'],\n settings: GuardSettings,\n cwd: string,\n statFile: (path: string) => { size: number; isFile: boolean } | undefined,\n): GuardDecision {\n if (input.path === undefined) return ALLOW;\n if (input.offsetLimited === true) return ALLOW; // already windowed — an economy move\n const path = isAbsolute(input.path) ? input.path : resolve(cwd, input.path);\n const stat = statFile(path);\n if (stat === undefined || !stat.isFile) return ALLOW; // let the tool surface its own error\n if (stat.size <= settings.thresholdBytes) return ALLOW;\n return denyOversized(path, stat.size, settings, 'Reading it raw');\n}\n\nfunction decideBash(\n input: GuardRequest['input'],\n settings: GuardSettings,\n cwd: string,\n statFile: (path: string) => { size: number; isFile: boolean } | undefined,\n): GuardDecision {\n if (input.command === undefined) return ALLOW;\n const command = input.command.trim();\n // A command already using smelt is the model doing the right thing — including the\n // exact replacement a previous deny suggested. Never intercept it (and never wrap a\n // wrapped pipeline a second time).\n if (/(^|[\\s/\"'=])smelt($|[\\s\"'])/.test(command)) return ALLOW;\n\n const words = simpleCommandWords(command);\n if (words === undefined || words.length === 0) return ALLOW; // not simple — unknowable pre-run\n\n const program = words[0]!.split('/').at(-1)!;\n\n if (program === 'cat') {\n const files = words.slice(1).filter((word) => word !== '--' && !word.startsWith('-'));\n for (const file of files) {\n const path = isAbsolute(file) ? file : resolve(cwd, file);\n const stat = statFile(path);\n if (stat === undefined || !stat.isFile) continue;\n if (stat.size > settings.thresholdBytes) {\n const decision = denyOversized(path, stat.size, settings, `\\`${command}\\``);\n // The suggestion is only a *faithful* replacement when cat named exactly this\n // one file; `cat a b` replaced by `smelt a` would silently drop b, so the\n // multi-file case keeps the reason (the model decides), drops the suggestion\n // (nothing may auto-substitute it), and says out loud that the named\n // replacement covers only the oversized file — a model following the reason\n // verbatim must not silently drop the others.\n return files.length === 1\n ? decision\n : {\n action: 'deny',\n reason:\n `${decision.reason ?? ''} Note: \\`${command}\\` names ` +\n `${String(files.length)} files and the replacement above covers only ` +\n `${path} — read the other file(s) separately (cat is fine for the ones ` +\n `under the threshold).`,\n };\n }\n }\n return ALLOW;\n }\n\n if ((program === 'grep' || program === 'rg') && settings.enforcement === 'rewrite') {\n const pattern = searchPattern(words);\n if (pattern === undefined) return ALLOW;\n // `--focus` on the wrap only where it distinguishes lines: a plain grep's every\n // output line contains the searched pattern, so focusing on it would protect the\n // entire output — zero elisions exactly when the output is large, plus an\n // over-budget exit — and the wrap lets the lexical planner keep the head and tail\n // and collapse the middle instead. A search with context prints non-matching\n // lines too, and there the pattern the guard already parsed is exactly the focus.\n // One derivation, `focusTermsFor`, states which is which; the same function\n // resolves a `--producer` hint in the ops seam, so both doors agree with this wrap.\n const focus = focusTermsFor(command);\n const focused = focus.map((term) => ` --focus ${shellQuote(term)}`).join('');\n const wrapped = `${command} | ${SMELT_CLI} --budget ${String(settings.budgetBytes)}${focused}`;\n return {\n action: 'deny',\n reason:\n `smelt guard (rewrite mode): \\`${program}\\` output size is unknowable before it runs, ` +\n `so pipe it through smelt instead. Run exactly: ${wrapped} — output within the ` +\n `budget passes through untouched; past it, elided regions leave <<smelt/v1 …>> ` +\n `markers${focused === '' ? '' : `, and the${focused} keeps every match and its context verbatim`}. ` +\n `${retrieveSentence(settings)}`,\n suggestion: wrapped,\n };\n }\n\n return ALLOW;\n}\n\n/**\n * The one sentence about getting elided bytes back — honest about the store: the\n * retrieval promise is only made when a persistent store is configured, because a\n * memory store dies with the process and `retrieve` then refuses (`resolveStoreRun`).\n */\nfunction retrieveSentence(settings: GuardSettings): string {\n return settings.persistentStore\n ? `\\`${SMELT_CLI} retrieve <hash>\\` prints any marker's bytes back, byte for byte.`\n : `\\`${SMELT_CLI} retrieve <hash>\\` can print a marker's bytes back once a persistent ` +\n `store is configured ({\"store\":{\"kind\":\"directory\",\"path\":…}} in smelt.config.json — ` +\n `\\`smelt hooks install\\` writes one); without it the elided bytes die with the ` +\n `smelt process.`;\n}\n\n/** The deny everything above the threshold gets: steering text plus the exact command. */\nfunction denyOversized(\n path: string,\n size: number,\n settings: GuardSettings,\n what: string,\n): GuardDecision {\n const replacement = `${SMELT_CLI} ${shellQuote(path)} --budget ${String(settings.budgetBytes)}`;\n return {\n action: 'deny',\n reason:\n `smelt guard: ${path} is ${String(size)} bytes — over the ${String(settings.thresholdBytes)}-byte ` +\n `threshold (smelt.config.json hooks.thresholdBytes). ${what} would spend context on bytes ` +\n `the task may not need. Run instead: ${replacement} --focus <what you are looking for> ` +\n `(repeat --focus per term; focused regions survive verbatim). Elided regions leave ` +\n `<<smelt/v1 …>> markers — ${retrieveSentence(settings)} A windowed read (offset/limit) ` +\n `of just the lines you need is also fine.`,\n suggestion: replacement,\n };\n}\n\n/* ------------------------------------------------------------------------------------\n * Process plumbing, for the shims that run as one\n * ---------------------------------------------------------------------------------- */\n\n/** True when this module is the file node was asked to run, not an import. */\nexport function isMainModule(moduleUrl: string): boolean {\n const entry = process.argv[1];\n if (entry === undefined) return false;\n try {\n return pathToFileURL(entry).href === moduleUrl;\n } catch {\n return false;\n }\n}\n\n/** Every byte of fd 0 to EOF, retrying EAGAIN — the same shape `cli/bin.ts` uses. */\nexport function readAllOfStdin(): string {\n const sleeper = new Int32Array(new SharedArrayBuffer(4));\n const chunks: Buffer[] = [];\n const chunk = Buffer.alloc(1 << 16);\n for (;;) {\n let bytesRead: number;\n try {\n bytesRead = readSync(0, chunk, 0, chunk.length, null);\n } catch (error) {\n const code = (error as { code?: string }).code;\n if (code === 'EAGAIN') {\n Atomics.wait(sleeper, 0, 0, 10);\n continue;\n }\n if (code === 'EOF') break;\n throw error;\n }\n if (bytesRead === 0) break;\n chunks.push(Buffer.from(chunk.subarray(0, bytesRead)));\n }\n return Buffer.concat(chunks).toString('utf8');\n}\n"]}
|
package/dist/hooks/guard-core.js
CHANGED
|
@@ -2,6 +2,13 @@ import { readSync, statSync, existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
4
|
import process from 'node:process';
|
|
5
|
+
import { focusTermsFor, searchPattern, shellQuote, simpleCommandWords } from './focus-terms.js';
|
|
6
|
+
/**
|
|
7
|
+
* The command parsing lives in `./focus-terms.ts` — a zero-import sibling, so the
|
|
8
|
+
* guard's no-library-import rule holds — and is re-exported here because this module
|
|
9
|
+
* is the published `hooks/guard-core` subpath every shim and the opencode plugin load.
|
|
10
|
+
*/
|
|
11
|
+
export { focusTermsFor, searchPattern, searchPatterns, shellQuote, simpleCommandWords, } from './focus-terms.js';
|
|
5
12
|
/**
|
|
6
13
|
* The guard core — one zero-dependency node module, shared by every harness shim.
|
|
7
14
|
*
|
|
@@ -20,7 +27,8 @@ import process from 'node:process';
|
|
|
20
27
|
* A guard that can brick a session on bad input is worse than no guard; the agent
|
|
21
28
|
* loses nothing but the optimization, and the warning says so.
|
|
22
29
|
* - **No library import on any path.** This module imports node builtins only —
|
|
23
|
-
* never `../index.ts`, never a planner, never web-tree-sitter
|
|
30
|
+
* never `../index.ts`, never a planner, never web-tree-sitter — plus its one
|
|
31
|
+
* zero-import sibling `./focus-terms.ts`, which owns the command parsing. The allow case is
|
|
24
32
|
* a stat and an exit; the research note
|
|
25
33
|
* (docs/research/2026-09-02-agent-enforcement.md § 5) budgets the always-on guard
|
|
26
34
|
* at tens of milliseconds, and loading grammar machinery here would spend that
|
|
@@ -291,18 +299,24 @@ function decideBash(input, settings, cwd, statFile) {
|
|
|
291
299
|
const pattern = searchPattern(words);
|
|
292
300
|
if (pattern === undefined)
|
|
293
301
|
return ALLOW;
|
|
294
|
-
//
|
|
295
|
-
// the searched pattern, so focusing on it would protect the
|
|
296
|
-
// elisions exactly when the output is large, plus an
|
|
297
|
-
// lets
|
|
298
|
-
//
|
|
299
|
-
|
|
302
|
+
// `--focus` on the wrap only where it distinguishes lines: a plain grep's every
|
|
303
|
+
// output line contains the searched pattern, so focusing on it would protect the
|
|
304
|
+
// entire output — zero elisions exactly when the output is large, plus an
|
|
305
|
+
// over-budget exit — and the wrap lets the lexical planner keep the head and tail
|
|
306
|
+
// and collapse the middle instead. A search with context prints non-matching
|
|
307
|
+
// lines too, and there the pattern the guard already parsed is exactly the focus.
|
|
308
|
+
// One derivation, `focusTermsFor`, states which is which; the same function
|
|
309
|
+
// resolves a `--producer` hint in the ops seam, so both doors agree with this wrap.
|
|
310
|
+
const focus = focusTermsFor(command);
|
|
311
|
+
const focused = focus.map((term) => ` --focus ${shellQuote(term)}`).join('');
|
|
312
|
+
const wrapped = `${command} | ${SMELT_CLI} --budget ${String(settings.budgetBytes)}${focused}`;
|
|
300
313
|
return {
|
|
301
314
|
action: 'deny',
|
|
302
315
|
reason: `smelt guard (rewrite mode): \`${program}\` output size is unknowable before it runs, ` +
|
|
303
316
|
`so pipe it through smelt instead. Run exactly: ${wrapped} — output within the ` +
|
|
304
317
|
`budget passes through untouched; past it, elided regions leave <<smelt/v1 …>> ` +
|
|
305
|
-
`markers
|
|
318
|
+
`markers${focused === '' ? '' : `, and the${focused} keeps every match and its context verbatim`}. ` +
|
|
319
|
+
`${retrieveSentence(settings)}`,
|
|
306
320
|
suggestion: wrapped,
|
|
307
321
|
};
|
|
308
322
|
}
|
|
@@ -335,118 +349,6 @@ function denyOversized(path, size, settings, what) {
|
|
|
335
349
|
suggestion: replacement,
|
|
336
350
|
};
|
|
337
351
|
}
|
|
338
|
-
/**
|
|
339
|
-
* Split a command into words IF it is one simple command: no pipes, no logic, no
|
|
340
|
-
* redirects, no substitutions, no expansions this code would have to model. Anything
|
|
341
|
-
* else returns `undefined` and the caller allows — the guard judges only what it can
|
|
342
|
-
* see whole.
|
|
343
|
-
*/
|
|
344
|
-
export function simpleCommandWords(command) {
|
|
345
|
-
const words = [];
|
|
346
|
-
let current = '';
|
|
347
|
-
let started = false;
|
|
348
|
-
let i = 0;
|
|
349
|
-
const push = () => {
|
|
350
|
-
if (started)
|
|
351
|
-
words.push(current);
|
|
352
|
-
current = '';
|
|
353
|
-
started = false;
|
|
354
|
-
};
|
|
355
|
-
while (i < command.length) {
|
|
356
|
-
const ch = command[i];
|
|
357
|
-
if ('|&;<>()`$\\\n*?~{}!'.includes(ch))
|
|
358
|
-
return undefined; // shell would interpret it
|
|
359
|
-
if (ch === "'" || ch === '"') {
|
|
360
|
-
const quote = ch;
|
|
361
|
-
i += 1;
|
|
362
|
-
started = true;
|
|
363
|
-
while (i < command.length && command[i] !== quote) {
|
|
364
|
-
if (quote === '"' && (command[i] === '$' || command[i] === '`' || command[i] === '\\')) {
|
|
365
|
-
return undefined; // expansions inside double quotes — not simple
|
|
366
|
-
}
|
|
367
|
-
current += command[i];
|
|
368
|
-
i += 1;
|
|
369
|
-
}
|
|
370
|
-
if (i >= command.length)
|
|
371
|
-
return undefined; // unterminated quote
|
|
372
|
-
i += 1;
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
if (ch === ' ' || ch === '\t') {
|
|
376
|
-
push();
|
|
377
|
-
i += 1;
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
current += ch;
|
|
381
|
-
started = true;
|
|
382
|
-
i += 1;
|
|
383
|
-
}
|
|
384
|
-
push();
|
|
385
|
-
return words;
|
|
386
|
-
}
|
|
387
|
-
/**
|
|
388
|
-
* The pattern a grep/rg invocation searches for: an explicit `-e`/`--regexp` value if
|
|
389
|
-
* given, else the first word that is not a flag or a flag's value. `undefined` when
|
|
390
|
-
* the parse is not sure — and unsure means allow, like everything else here.
|
|
391
|
-
*/
|
|
392
|
-
export function searchPattern(words) {
|
|
393
|
-
const takesValue = new Set([
|
|
394
|
-
'-e',
|
|
395
|
-
'--regexp',
|
|
396
|
-
'-f',
|
|
397
|
-
'--file',
|
|
398
|
-
'-m',
|
|
399
|
-
'--max-count',
|
|
400
|
-
'-A',
|
|
401
|
-
'--after-context',
|
|
402
|
-
'-B',
|
|
403
|
-
'--before-context',
|
|
404
|
-
'-C',
|
|
405
|
-
'--context',
|
|
406
|
-
'-d',
|
|
407
|
-
'--directories',
|
|
408
|
-
'-D',
|
|
409
|
-
'--devices',
|
|
410
|
-
'--include',
|
|
411
|
-
'--exclude',
|
|
412
|
-
'--exclude-dir',
|
|
413
|
-
'-t',
|
|
414
|
-
'--type',
|
|
415
|
-
'-T',
|
|
416
|
-
'--type-not',
|
|
417
|
-
'-g',
|
|
418
|
-
'--glob',
|
|
419
|
-
'--iglob',
|
|
420
|
-
'-j',
|
|
421
|
-
'--threads',
|
|
422
|
-
'--color',
|
|
423
|
-
'--colour',
|
|
424
|
-
]);
|
|
425
|
-
let i = 1;
|
|
426
|
-
while (i < words.length) {
|
|
427
|
-
const word = words[i];
|
|
428
|
-
if (word === '--')
|
|
429
|
-
return words[i + 1];
|
|
430
|
-
if (word === '-e' || word === '--regexp')
|
|
431
|
-
return words[i + 1];
|
|
432
|
-
if (word.startsWith('--') && word.includes('=')) {
|
|
433
|
-
i += 1;
|
|
434
|
-
continue;
|
|
435
|
-
}
|
|
436
|
-
if (word.startsWith('-') && word.length > 1) {
|
|
437
|
-
i += takesValue.has(word) ? 2 : 1;
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
return word;
|
|
441
|
-
}
|
|
442
|
-
return undefined;
|
|
443
|
-
}
|
|
444
|
-
/** Single-quote a value for `sh` unless it is plainly safe bare. */
|
|
445
|
-
export function shellQuote(value) {
|
|
446
|
-
if (/^[A-Za-z0-9_./:=-]+$/.test(value))
|
|
447
|
-
return value;
|
|
448
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
449
|
-
}
|
|
450
352
|
/* ------------------------------------------------------------------------------------
|
|
451
353
|
* Process plumbing, for the shims that run as one
|
|
452
354
|
* ---------------------------------------------------------------------------------- */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guard-core.js","sourceRoot":"","sources":["../../src/hooks/guard-core.ts"],"names":[],"mappings":"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","sourcesContent":["import { readSync, statSync, existsSync, readFileSync } from 'node:fs';\nimport { dirname, isAbsolute, join, resolve } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport process from 'node:process';\n\n/**\n * The guard core — one zero-dependency node module, shared by every harness shim.\n *\n * Contract: a caller hands `decide` a {@link GuardRequest}\n * (`{ tool, input: { path?, command?, offsetLimited? } }`) with the settings\n * {@link readGuardSettings} read, and gets one {@link GuardDecision} back —\n * `{ action: \"allow\" | \"deny\", reason?, suggestion? }`. Each shim translates that into\n * its harness's own schema (exit 2, `permissionDecision`, `cancel:true`, …), and the\n * opencode plugin — the one harness whose hook API is JavaScript — imports this module\n * at hook time and calls the same two functions.\n *\n * Two properties are load-bearing and guarded:\n *\n * - **Fail open, loudly.** Malformed stdin, a malformed config, an unstatable path —\n * every degenerate input produces `{\"action\":\"allow\"}` plus a warning on stderr.\n * A guard that can brick a session on bad input is worse than no guard; the agent\n * loses nothing but the optimization, and the warning says so.\n * - **No library import on any path.** This module imports node builtins only —\n * never `../index.ts`, never a planner, never web-tree-sitter. The allow case is\n * a stat and an exit; the research note\n * (docs/research/2026-09-02-agent-enforcement.md § 5) budgets the always-on guard\n * at tens of milliseconds, and loading grammar machinery here would spend that\n * budget before deciding anything. The smelt run itself is only ever paid by the\n * *replacement* command the model runs after a deny (or the rewritten command).\n *\n * Config: the nearest `smelt.config.json` (walking up from the cwd, same discovery\n * as the CLI) may carry a `hooks` block — `thresholdBytes` and `enforcement` — plus\n * the `defaultBudgetBytes` the suggested command quotes. This file reads that config\n * with its own tolerant reader instead of importing `cli/config.ts`: the CLI's strict\n * parser sits on the planner import graph, and a *guard* must fail open where the CLI\n * correctly refuses. `test/hooks-guard-core.test.ts` pins the two readers to the same\n * key names and defaults, so they cannot drift apart silently.\n */\n\n/** Deny threshold when no config says otherwise: reads at or under this pass untouched. */\nexport const DEFAULT_THRESHOLD_BYTES = 8192;\n\n/**\n * The `--budget` the suggested replacement command quotes when no config carries\n * `defaultBudgetBytes`. A suggestion default, not a smelt default: the CLI itself\n * still refuses to run without an explicit budget from a flag or the config.\n */\nexport const DEFAULT_SUGGESTION_BUDGET_BYTES = 8000;\n\n/** The `hooks.enforcement` values `smelt.config.json` may carry. Deny is the default. */\nexport const ENFORCEMENT_MODES = ['deny', 'rewrite'] as const;\nexport type EnforcementMode = (typeof ENFORCEMENT_MODES)[number];\n\n/** The config file this guard discovers, by the same name the CLI uses. */\nexport const GUARD_CONFIG_FILE_NAME = 'smelt.config.json';\n\n/**\n * The runnable CLI name every reason and suggestion quotes. A local (non-global)\n * `npm install @smeltjs/core` puts no `smelt` on anyone's PATH — the installer wires\n * every shim as `node \"<dist path>\"` for exactly that reason — so a suggestion\n * saying bare `smelt` would exit 127 the moment the model (or a rewrite-mode\n * harness) ran it. When this module's sibling `cli/bin.js` exists — the shipped\n * `dist/` layout every real run executes from — the command names it through `node`\n * explicitly; the bare name is only the fallback for layouts where the sibling is\n * absent (the source tree under the test runner).\n */\nexport function smeltCliCommand(): string {\n try {\n const bin = join(dirname(fileURLToPath(import.meta.url)), '..', 'cli', 'bin.js');\n if (existsSync(bin)) return `node ${shellQuote(bin)}`;\n } catch {\n // fall through to the PATH name\n }\n return 'smelt';\n}\n\nconst SMELT_CLI = smeltCliCommand();\n\n/** What a shim hands the guard core: the harness schema already mapped away. */\nexport interface GuardRequest {\n /** `'Read'` for a file-read tool, `'Bash'` for a shell tool; anything else passes. */\n readonly tool: string;\n readonly input: {\n /** The file a Read-shaped tool targets. Relative paths resolve against the cwd. */\n readonly path?: string;\n /** The command a Bash-shaped tool would run, verbatim. */\n readonly command?: string;\n /**\n * True when the read is already windowed (offset/limit given). A windowed read\n * of a huge file is an economy move — it is always allowed, whatever the size.\n */\n readonly offsetLimited?: boolean;\n };\n}\n\n/** The guard's whole answer. `suggestion`, when present, is an executable command. */\nexport interface GuardDecision {\n readonly action: 'allow' | 'deny';\n /** Why, written to steer: names the exact replacement command and `smelt retrieve`. */\n readonly reason?: string;\n /**\n * A command that faithfully replaces the denied one — `smelt <path> --budget <n>`\n * for a raw read, the original pipeline with ` | smelt …` appended for a search.\n * Only emitted when running it preserves the intent of the original call, which is\n * exactly the condition under which a rewrite-mode shim may substitute it via\n * `updatedInput`. Absent on decisions that need the model's judgement instead.\n */\n readonly suggestion?: string;\n}\n\n/** The guard's merged settings: config values where sane, defaults where not. */\nexport interface GuardSettings {\n readonly thresholdBytes: number;\n readonly enforcement: EnforcementMode;\n /** Quoted in every suggested command, so the model runs a complete line. */\n readonly budgetBytes: number;\n /**\n * True when the config carries a directory store. `smelt retrieve <hash>` only\n * works across processes with a persistent store (the CLI's default is memory,\n * which dies with the process that elided), so a deny reason may only *promise*\n * retrieval when this is true — otherwise it says what to configure instead.\n */\n readonly persistentStore: boolean;\n}\n\nexport const DEFAULT_GUARD_SETTINGS: GuardSettings = {\n thresholdBytes: DEFAULT_THRESHOLD_BYTES,\n enforcement: 'deny',\n budgetBytes: DEFAULT_SUGGESTION_BUDGET_BYTES,\n persistentStore: false,\n};\n\n/**\n * Read the nearest `smelt.config.json`'s guard-relevant fields, tolerantly.\n *\n * Tolerant is a deliberate divergence from the CLI: `smelt` refuses a malformed\n * config because a silently skipped setting is a setting the user believed was in\n * force — but this code runs inside somebody's *session*, before every Read, and a\n * guard that turns a config typo into a hard-down harness has failed worse than the\n * typo. So: any unreadable or ill-typed field falls back to its default, and `warn`\n * receives one line saying which file and why — visible in the harness's hook debug\n * output, never fatal.\n */\nexport function readGuardSettings(cwd: string, warn: (text: string) => void): GuardSettings {\n const path = findGuardConfigFile(cwd);\n if (path === undefined) return DEFAULT_GUARD_SETTINGS;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(path, 'utf8'));\n } catch (cause) {\n warn(\n `smelt guard: ${path} is not readable JSON ` +\n `(${cause instanceof Error ? cause.message : String(cause)}) — ` +\n `guarding with defaults instead. \\`smelt\\` itself will refuse this config.`,\n );\n return DEFAULT_GUARD_SETTINGS;\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n warn(`smelt guard: ${path} is not a JSON object — guarding with defaults instead.`);\n return DEFAULT_GUARD_SETTINGS;\n }\n const fields = parsed as Record<string, unknown>;\n const hooks =\n typeof fields['hooks'] === 'object' &&\n fields['hooks'] !== null &&\n !Array.isArray(fields['hooks'])\n ? (fields['hooks'] as Record<string, unknown>)\n : {};\n\n return {\n thresholdBytes: positiveInteger(\n hooks['thresholdBytes'],\n DEFAULT_THRESHOLD_BYTES,\n `${path}: hooks.thresholdBytes`,\n warn,\n ),\n enforcement: enforcementMode(hooks['enforcement'], `${path}: hooks.enforcement`, warn),\n budgetBytes: positiveInteger(\n fields['defaultBudgetBytes'],\n DEFAULT_SUGGESTION_BUDGET_BYTES,\n `${path}: defaultBudgetBytes`,\n warn,\n ),\n persistentStore: isDirectoryStore(fields['store']),\n };\n}\n\n/** True for a well-formed `{\"kind\":\"directory\",\"path\":…}` store block; no warning\n * otherwise — an absent or memory store is a valid (just non-persistent) choice. */\nfunction isDirectoryStore(value: unknown): boolean {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const fields = value as Record<string, unknown>;\n return fields['kind'] === 'directory' && typeof fields['path'] === 'string';\n}\n\nfunction positiveInteger(\n value: unknown,\n fallback: number,\n what: string,\n warn: (text: string) => void,\n): number {\n if (value === undefined) return fallback;\n if (typeof value === 'number' && Number.isInteger(value) && value > 0) return value;\n warn(\n `smelt guard: ${what} must be a positive integer, got ${JSON.stringify(value)} — ` +\n `using ${String(fallback)}.`,\n );\n return fallback;\n}\n\nfunction enforcementMode(\n value: unknown,\n what: string,\n warn: (text: string) => void,\n): EnforcementMode {\n if (value === undefined) return 'deny';\n if (value === 'deny' || value === 'rewrite') return value;\n warn(\n `smelt guard: ${what} must be \"deny\" or \"rewrite\", got ${JSON.stringify(value)} — ` +\n `using \"deny\".`,\n );\n return 'deny';\n}\n\n/** The same upward walk `cli/config.ts` does, re-implemented to keep this module tiny. */\nexport function findGuardConfigFile(cwd: string): string | undefined {\n let dir = resolve(cwd);\n for (;;) {\n const candidate = join(dir, GUARD_CONFIG_FILE_NAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n/**\n * Parse one {@link GuardRequest} *document* — the request shape as JSON, for an\n * adapter that receives it over a pipe rather than building it in-process.\n * `undefined` means malformed, and a caller that gets it allows and warns: the same\n * fail-open rule the rest of this module lives under.\n */\nexport function parseGuardRequest(text: string): GuardRequest | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return undefined;\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return undefined;\n const fields = parsed as Record<string, unknown>;\n if (typeof fields['tool'] !== 'string') return undefined;\n const input = fields['input'];\n if (typeof input !== 'object' || input === null || Array.isArray(input)) return undefined;\n const inputFields = input as Record<string, unknown>;\n const path = inputFields['path'];\n const command = inputFields['command'];\n const offsetLimited = inputFields['offsetLimited'];\n if (path !== undefined && typeof path !== 'string') return undefined;\n if (command !== undefined && typeof command !== 'string') return undefined;\n if (offsetLimited !== undefined && typeof offsetLimited !== 'boolean') return undefined;\n return {\n tool: fields['tool'],\n input: {\n ...(path === undefined ? {} : { path }),\n ...(command === undefined ? {} : { command }),\n ...(offsetLimited === undefined ? {} : { offsetLimited }),\n },\n };\n}\n\nconst ALLOW: GuardDecision = { action: 'allow' };\n\n/**\n * The decision, pure given a stat function — so tests exercise every branch without\n * a filesystem, and the script wires `statSync` in.\n *\n * The shape of the rules, from the research note (§ 5, \"the ~8 KB threshold,\n * validated\", with both amendments):\n *\n * - **Read**: stat the exact target; deny only above the threshold, and never when\n * the read is already windowed (`offsetLimited`) — a windowed read of a huge file\n * is an economy move.\n * - **Bash**: only a *simple* command whose subject is a statable named file can be\n * judged pre-run. `cat <file>` above the threshold is denied with the faithful\n * replacement; `grep`/`rg` output is unknowable pre-run, so it passes in deny\n * mode and is wrapped (` | smelt --budget … --focus <pattern>`) only under\n * `hooks.enforcement: \"rewrite\"`. Pipelines, redirects, substitutions — anything\n * this parser cannot be sure about — pass untouched. Fail open, always.\n */\nexport function decide(\n request: GuardRequest,\n settings: GuardSettings,\n cwd: string,\n statFile: (path: string) => { size: number; isFile: boolean } | undefined = statFileReal,\n): GuardDecision {\n if (request.tool === 'Read') {\n return decideRead(request.input, settings, cwd, statFile);\n }\n if (request.tool === 'Bash') {\n return decideBash(request.input, settings, cwd, statFile);\n }\n return ALLOW;\n}\n\nfunction statFileReal(path: string): { size: number; isFile: boolean } | undefined {\n try {\n const stat = statSync(path);\n return { size: stat.size, isFile: stat.isFile() };\n } catch {\n return undefined;\n }\n}\n\nfunction decideRead(\n input: GuardRequest['input'],\n settings: GuardSettings,\n cwd: string,\n statFile: (path: string) => { size: number; isFile: boolean } | undefined,\n): GuardDecision {\n if (input.path === undefined) return ALLOW;\n if (input.offsetLimited === true) return ALLOW; // already windowed — an economy move\n const path = isAbsolute(input.path) ? input.path : resolve(cwd, input.path);\n const stat = statFile(path);\n if (stat === undefined || !stat.isFile) return ALLOW; // let the tool surface its own error\n if (stat.size <= settings.thresholdBytes) return ALLOW;\n return denyOversized(path, stat.size, settings, 'Reading it raw');\n}\n\nfunction decideBash(\n input: GuardRequest['input'],\n settings: GuardSettings,\n cwd: string,\n statFile: (path: string) => { size: number; isFile: boolean } | undefined,\n): GuardDecision {\n if (input.command === undefined) return ALLOW;\n const command = input.command.trim();\n // A command already using smelt is the model doing the right thing — including the\n // exact replacement a previous deny suggested. Never intercept it (and never wrap a\n // wrapped pipeline a second time).\n if (/(^|[\\s/\"'=])smelt($|[\\s\"'])/.test(command)) return ALLOW;\n\n const words = simpleCommandWords(command);\n if (words === undefined || words.length === 0) return ALLOW; // not simple — unknowable pre-run\n\n const program = words[0]!.split('/').at(-1)!;\n\n if (program === 'cat') {\n const files = words.slice(1).filter((word) => word !== '--' && !word.startsWith('-'));\n for (const file of files) {\n const path = isAbsolute(file) ? file : resolve(cwd, file);\n const stat = statFile(path);\n if (stat === undefined || !stat.isFile) continue;\n if (stat.size > settings.thresholdBytes) {\n const decision = denyOversized(path, stat.size, settings, `\\`${command}\\``);\n // The suggestion is only a *faithful* replacement when cat named exactly this\n // one file; `cat a b` replaced by `smelt a` would silently drop b, so the\n // multi-file case keeps the reason (the model decides), drops the suggestion\n // (nothing may auto-substitute it), and says out loud that the named\n // replacement covers only the oversized file — a model following the reason\n // verbatim must not silently drop the others.\n return files.length === 1\n ? decision\n : {\n action: 'deny',\n reason:\n `${decision.reason ?? ''} Note: \\`${command}\\` names ` +\n `${String(files.length)} files and the replacement above covers only ` +\n `${path} — read the other file(s) separately (cat is fine for the ones ` +\n `under the threshold).`,\n };\n }\n }\n return ALLOW;\n }\n\n if ((program === 'grep' || program === 'rg') && settings.enforcement === 'rewrite') {\n const pattern = searchPattern(words);\n if (pattern === undefined) return ALLOW;\n // Deliberately no `--focus` on the wrap: a plain grep's every output line contains\n // the searched pattern, so focusing on it would protect the entire output — zero\n // elisions exactly when the output is large, plus an over-budget exit. The wrap\n // lets smelt's lexical planner keep the head and tail and collapse the middle\n // into retrievable markers instead.\n const wrapped = `${command} | ${SMELT_CLI} --budget ${String(settings.budgetBytes)}`;\n return {\n action: 'deny',\n reason:\n `smelt guard (rewrite mode): \\`${program}\\` output size is unknowable before it runs, ` +\n `so pipe it through smelt instead. Run exactly: ${wrapped} — output within the ` +\n `budget passes through untouched; past it, elided regions leave <<smelt/v1 …>> ` +\n `markers. ${retrieveSentence(settings)}`,\n suggestion: wrapped,\n };\n }\n\n return ALLOW;\n}\n\n/**\n * The one sentence about getting elided bytes back — honest about the store: the\n * retrieval promise is only made when a persistent store is configured, because a\n * memory store dies with the process and `retrieve` then refuses (`resolveStoreRun`).\n */\nfunction retrieveSentence(settings: GuardSettings): string {\n return settings.persistentStore\n ? `\\`${SMELT_CLI} retrieve <hash>\\` prints any marker's bytes back, byte for byte.`\n : `\\`${SMELT_CLI} retrieve <hash>\\` can print a marker's bytes back once a persistent ` +\n `store is configured ({\"store\":{\"kind\":\"directory\",\"path\":…}} in smelt.config.json — ` +\n `\\`smelt hooks install\\` writes one); without it the elided bytes die with the ` +\n `smelt process.`;\n}\n\n/** The deny everything above the threshold gets: steering text plus the exact command. */\nfunction denyOversized(\n path: string,\n size: number,\n settings: GuardSettings,\n what: string,\n): GuardDecision {\n const replacement = `${SMELT_CLI} ${shellQuote(path)} --budget ${String(settings.budgetBytes)}`;\n return {\n action: 'deny',\n reason:\n `smelt guard: ${path} is ${String(size)} bytes — over the ${String(settings.thresholdBytes)}-byte ` +\n `threshold (smelt.config.json hooks.thresholdBytes). ${what} would spend context on bytes ` +\n `the task may not need. Run instead: ${replacement} --focus <what you are looking for> ` +\n `(repeat --focus per term; focused regions survive verbatim). Elided regions leave ` +\n `<<smelt/v1 …>> markers — ${retrieveSentence(settings)} A windowed read (offset/limit) ` +\n `of just the lines you need is also fine.`,\n suggestion: replacement,\n };\n}\n\n/**\n * Split a command into words IF it is one simple command: no pipes, no logic, no\n * redirects, no substitutions, no expansions this code would have to model. Anything\n * else returns `undefined` and the caller allows — the guard judges only what it can\n * see whole.\n */\nexport function simpleCommandWords(command: string): readonly string[] | undefined {\n const words: string[] = [];\n let current = '';\n let started = false;\n let i = 0;\n const push = (): void => {\n if (started) words.push(current);\n current = '';\n started = false;\n };\n while (i < command.length) {\n const ch = command[i]!;\n if ('|&;<>()`$\\\\\\n*?~{}!'.includes(ch)) return undefined; // shell would interpret it\n if (ch === \"'\" || ch === '\"') {\n const quote = ch;\n i += 1;\n started = true;\n while (i < command.length && command[i] !== quote) {\n if (quote === '\"' && (command[i] === '$' || command[i] === '`' || command[i] === '\\\\')) {\n return undefined; // expansions inside double quotes — not simple\n }\n current += command[i]!;\n i += 1;\n }\n if (i >= command.length) return undefined; // unterminated quote\n i += 1;\n continue;\n }\n if (ch === ' ' || ch === '\\t') {\n push();\n i += 1;\n continue;\n }\n current += ch;\n started = true;\n i += 1;\n }\n push();\n return words;\n}\n\n/**\n * The pattern a grep/rg invocation searches for: an explicit `-e`/`--regexp` value if\n * given, else the first word that is not a flag or a flag's value. `undefined` when\n * the parse is not sure — and unsure means allow, like everything else here.\n */\nexport function searchPattern(words: readonly string[]): string | undefined {\n const takesValue = new Set([\n '-e',\n '--regexp',\n '-f',\n '--file',\n '-m',\n '--max-count',\n '-A',\n '--after-context',\n '-B',\n '--before-context',\n '-C',\n '--context',\n '-d',\n '--directories',\n '-D',\n '--devices',\n '--include',\n '--exclude',\n '--exclude-dir',\n '-t',\n '--type',\n '-T',\n '--type-not',\n '-g',\n '--glob',\n '--iglob',\n '-j',\n '--threads',\n '--color',\n '--colour',\n ]);\n let i = 1;\n while (i < words.length) {\n const word = words[i]!;\n if (word === '--') return words[i + 1];\n if (word === '-e' || word === '--regexp') return words[i + 1];\n if (word.startsWith('--') && word.includes('=')) {\n i += 1;\n continue;\n }\n if (word.startsWith('-') && word.length > 1) {\n i += takesValue.has(word) ? 2 : 1;\n continue;\n }\n return word;\n }\n return undefined;\n}\n\n/** Single-quote a value for `sh` unless it is plainly safe bare. */\nexport function shellQuote(value: string): string {\n if (/^[A-Za-z0-9_./:=-]+$/.test(value)) return value;\n return `'${value.replaceAll(\"'\", `'\"'\"'`)}'`;\n}\n\n/* ------------------------------------------------------------------------------------\n * Process plumbing, for the shims that run as one\n * ---------------------------------------------------------------------------------- */\n\n/** True when this module is the file node was asked to run, not an import. */\nexport function isMainModule(moduleUrl: string): boolean {\n const entry = process.argv[1];\n if (entry === undefined) return false;\n try {\n return pathToFileURL(entry).href === moduleUrl;\n } catch {\n return false;\n }\n}\n\n/** Every byte of fd 0 to EOF, retrying EAGAIN — the same shape `cli/bin.ts` uses. */\nexport function readAllOfStdin(): string {\n const sleeper = new Int32Array(new SharedArrayBuffer(4));\n const chunks: Buffer[] = [];\n const chunk = Buffer.alloc(1 << 16);\n for (;;) {\n let bytesRead: number;\n try {\n bytesRead = readSync(0, chunk, 0, chunk.length, null);\n } catch (error) {\n const code = (error as { code?: string }).code;\n if (code === 'EAGAIN') {\n Atomics.wait(sleeper, 0, 0, 10);\n continue;\n }\n if (code === 'EOF') break;\n throw error;\n }\n if (bytesRead === 0) break;\n chunks.push(Buffer.from(chunk.subarray(0, bytesRead)));\n }\n return Buffer.concat(chunks).toString('utf8');\n}\n"]}
|
|
1
|
+
{"version":3,"file":"guard-core.js","sourceRoot":"","sources":["../../src/hooks/guard-core.ts"],"names":[],"mappings":"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","sourcesContent":["import { readSync, statSync, existsSync, readFileSync } from 'node:fs';\nimport { dirname, isAbsolute, join, resolve } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\nimport process from 'node:process';\n\nimport { focusTermsFor, searchPattern, shellQuote, simpleCommandWords } from './focus-terms.ts';\n\n/**\n * The command parsing lives in `./focus-terms.ts` — a zero-import sibling, so the\n * guard's no-library-import rule holds — and is re-exported here because this module\n * is the published `hooks/guard-core` subpath every shim and the opencode plugin load.\n */\nexport {\n focusTermsFor,\n searchPattern,\n searchPatterns,\n shellQuote,\n simpleCommandWords,\n} from './focus-terms.ts';\n\n/**\n * The guard core — one zero-dependency node module, shared by every harness shim.\n *\n * Contract: a caller hands `decide` a {@link GuardRequest}\n * (`{ tool, input: { path?, command?, offsetLimited? } }`) with the settings\n * {@link readGuardSettings} read, and gets one {@link GuardDecision} back —\n * `{ action: \"allow\" | \"deny\", reason?, suggestion? }`. Each shim translates that into\n * its harness's own schema (exit 2, `permissionDecision`, `cancel:true`, …), and the\n * opencode plugin — the one harness whose hook API is JavaScript — imports this module\n * at hook time and calls the same two functions.\n *\n * Two properties are load-bearing and guarded:\n *\n * - **Fail open, loudly.** Malformed stdin, a malformed config, an unstatable path —\n * every degenerate input produces `{\"action\":\"allow\"}` plus a warning on stderr.\n * A guard that can brick a session on bad input is worse than no guard; the agent\n * loses nothing but the optimization, and the warning says so.\n * - **No library import on any path.** This module imports node builtins only —\n * never `../index.ts`, never a planner, never web-tree-sitter — plus its one\n * zero-import sibling `./focus-terms.ts`, which owns the command parsing. The allow case is\n * a stat and an exit; the research note\n * (docs/research/2026-09-02-agent-enforcement.md § 5) budgets the always-on guard\n * at tens of milliseconds, and loading grammar machinery here would spend that\n * budget before deciding anything. The smelt run itself is only ever paid by the\n * *replacement* command the model runs after a deny (or the rewritten command).\n *\n * Config: the nearest `smelt.config.json` (walking up from the cwd, same discovery\n * as the CLI) may carry a `hooks` block — `thresholdBytes` and `enforcement` — plus\n * the `defaultBudgetBytes` the suggested command quotes. This file reads that config\n * with its own tolerant reader instead of importing `cli/config.ts`: the CLI's strict\n * parser sits on the planner import graph, and a *guard* must fail open where the CLI\n * correctly refuses. `test/hooks-guard-core.test.ts` pins the two readers to the same\n * key names and defaults, so they cannot drift apart silently.\n */\n\n/** Deny threshold when no config says otherwise: reads at or under this pass untouched. */\nexport const DEFAULT_THRESHOLD_BYTES = 8192;\n\n/**\n * The `--budget` the suggested replacement command quotes when no config carries\n * `defaultBudgetBytes`. A suggestion default, not a smelt default: the CLI itself\n * still refuses to run without an explicit budget from a flag or the config.\n */\nexport const DEFAULT_SUGGESTION_BUDGET_BYTES = 8000;\n\n/** The `hooks.enforcement` values `smelt.config.json` may carry. Deny is the default. */\nexport const ENFORCEMENT_MODES = ['deny', 'rewrite'] as const;\nexport type EnforcementMode = (typeof ENFORCEMENT_MODES)[number];\n\n/** The config file this guard discovers, by the same name the CLI uses. */\nexport const GUARD_CONFIG_FILE_NAME = 'smelt.config.json';\n\n/**\n * The runnable CLI name every reason and suggestion quotes. A local (non-global)\n * `npm install @smeltjs/core` puts no `smelt` on anyone's PATH — the installer wires\n * every shim as `node \"<dist path>\"` for exactly that reason — so a suggestion\n * saying bare `smelt` would exit 127 the moment the model (or a rewrite-mode\n * harness) ran it. When this module's sibling `cli/bin.js` exists — the shipped\n * `dist/` layout every real run executes from — the command names it through `node`\n * explicitly; the bare name is only the fallback for layouts where the sibling is\n * absent (the source tree under the test runner).\n */\nexport function smeltCliCommand(): string {\n try {\n const bin = join(dirname(fileURLToPath(import.meta.url)), '..', 'cli', 'bin.js');\n if (existsSync(bin)) return `node ${shellQuote(bin)}`;\n } catch {\n // fall through to the PATH name\n }\n return 'smelt';\n}\n\nconst SMELT_CLI = smeltCliCommand();\n\n/** What a shim hands the guard core: the harness schema already mapped away. */\nexport interface GuardRequest {\n /** `'Read'` for a file-read tool, `'Bash'` for a shell tool; anything else passes. */\n readonly tool: string;\n readonly input: {\n /** The file a Read-shaped tool targets. Relative paths resolve against the cwd. */\n readonly path?: string;\n /** The command a Bash-shaped tool would run, verbatim. */\n readonly command?: string;\n /**\n * True when the read is already windowed (offset/limit given). A windowed read\n * of a huge file is an economy move — it is always allowed, whatever the size.\n */\n readonly offsetLimited?: boolean;\n };\n}\n\n/** The guard's whole answer. `suggestion`, when present, is an executable command. */\nexport interface GuardDecision {\n readonly action: 'allow' | 'deny';\n /** Why, written to steer: names the exact replacement command and `smelt retrieve`. */\n readonly reason?: string;\n /**\n * A command that faithfully replaces the denied one — `smelt <path> --budget <n>`\n * for a raw read, the original pipeline with ` | smelt …` appended for a search.\n * Only emitted when running it preserves the intent of the original call, which is\n * exactly the condition under which a rewrite-mode shim may substitute it via\n * `updatedInput`. Absent on decisions that need the model's judgement instead.\n */\n readonly suggestion?: string;\n}\n\n/** The guard's merged settings: config values where sane, defaults where not. */\nexport interface GuardSettings {\n readonly thresholdBytes: number;\n readonly enforcement: EnforcementMode;\n /** Quoted in every suggested command, so the model runs a complete line. */\n readonly budgetBytes: number;\n /**\n * True when the config carries a directory store. `smelt retrieve <hash>` only\n * works across processes with a persistent store (the CLI's default is memory,\n * which dies with the process that elided), so a deny reason may only *promise*\n * retrieval when this is true — otherwise it says what to configure instead.\n */\n readonly persistentStore: boolean;\n}\n\nexport const DEFAULT_GUARD_SETTINGS: GuardSettings = {\n thresholdBytes: DEFAULT_THRESHOLD_BYTES,\n enforcement: 'deny',\n budgetBytes: DEFAULT_SUGGESTION_BUDGET_BYTES,\n persistentStore: false,\n};\n\n/**\n * Read the nearest `smelt.config.json`'s guard-relevant fields, tolerantly.\n *\n * Tolerant is a deliberate divergence from the CLI: `smelt` refuses a malformed\n * config because a silently skipped setting is a setting the user believed was in\n * force — but this code runs inside somebody's *session*, before every Read, and a\n * guard that turns a config typo into a hard-down harness has failed worse than the\n * typo. So: any unreadable or ill-typed field falls back to its default, and `warn`\n * receives one line saying which file and why — visible in the harness's hook debug\n * output, never fatal.\n */\nexport function readGuardSettings(cwd: string, warn: (text: string) => void): GuardSettings {\n const path = findGuardConfigFile(cwd);\n if (path === undefined) return DEFAULT_GUARD_SETTINGS;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(path, 'utf8'));\n } catch (cause) {\n warn(\n `smelt guard: ${path} is not readable JSON ` +\n `(${cause instanceof Error ? cause.message : String(cause)}) — ` +\n `guarding with defaults instead. \\`smelt\\` itself will refuse this config.`,\n );\n return DEFAULT_GUARD_SETTINGS;\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n warn(`smelt guard: ${path} is not a JSON object — guarding with defaults instead.`);\n return DEFAULT_GUARD_SETTINGS;\n }\n const fields = parsed as Record<string, unknown>;\n const hooks =\n typeof fields['hooks'] === 'object' &&\n fields['hooks'] !== null &&\n !Array.isArray(fields['hooks'])\n ? (fields['hooks'] as Record<string, unknown>)\n : {};\n\n return {\n thresholdBytes: positiveInteger(\n hooks['thresholdBytes'],\n DEFAULT_THRESHOLD_BYTES,\n `${path}: hooks.thresholdBytes`,\n warn,\n ),\n enforcement: enforcementMode(hooks['enforcement'], `${path}: hooks.enforcement`, warn),\n budgetBytes: positiveInteger(\n fields['defaultBudgetBytes'],\n DEFAULT_SUGGESTION_BUDGET_BYTES,\n `${path}: defaultBudgetBytes`,\n warn,\n ),\n persistentStore: isDirectoryStore(fields['store']),\n };\n}\n\n/** True for a well-formed `{\"kind\":\"directory\",\"path\":…}` store block; no warning\n * otherwise — an absent or memory store is a valid (just non-persistent) choice. */\nfunction isDirectoryStore(value: unknown): boolean {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const fields = value as Record<string, unknown>;\n return fields['kind'] === 'directory' && typeof fields['path'] === 'string';\n}\n\nfunction positiveInteger(\n value: unknown,\n fallback: number,\n what: string,\n warn: (text: string) => void,\n): number {\n if (value === undefined) return fallback;\n if (typeof value === 'number' && Number.isInteger(value) && value > 0) return value;\n warn(\n `smelt guard: ${what} must be a positive integer, got ${JSON.stringify(value)} — ` +\n `using ${String(fallback)}.`,\n );\n return fallback;\n}\n\nfunction enforcementMode(\n value: unknown,\n what: string,\n warn: (text: string) => void,\n): EnforcementMode {\n if (value === undefined) return 'deny';\n if (value === 'deny' || value === 'rewrite') return value;\n warn(\n `smelt guard: ${what} must be \"deny\" or \"rewrite\", got ${JSON.stringify(value)} — ` +\n `using \"deny\".`,\n );\n return 'deny';\n}\n\n/** The same upward walk `cli/config.ts` does, re-implemented to keep this module tiny. */\nexport function findGuardConfigFile(cwd: string): string | undefined {\n let dir = resolve(cwd);\n for (;;) {\n const candidate = join(dir, GUARD_CONFIG_FILE_NAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n/**\n * Parse one {@link GuardRequest} *document* — the request shape as JSON, for an\n * adapter that receives it over a pipe rather than building it in-process.\n * `undefined` means malformed, and a caller that gets it allows and warns: the same\n * fail-open rule the rest of this module lives under.\n */\nexport function parseGuardRequest(text: string): GuardRequest | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return undefined;\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return undefined;\n const fields = parsed as Record<string, unknown>;\n if (typeof fields['tool'] !== 'string') return undefined;\n const input = fields['input'];\n if (typeof input !== 'object' || input === null || Array.isArray(input)) return undefined;\n const inputFields = input as Record<string, unknown>;\n const path = inputFields['path'];\n const command = inputFields['command'];\n const offsetLimited = inputFields['offsetLimited'];\n if (path !== undefined && typeof path !== 'string') return undefined;\n if (command !== undefined && typeof command !== 'string') return undefined;\n if (offsetLimited !== undefined && typeof offsetLimited !== 'boolean') return undefined;\n return {\n tool: fields['tool'],\n input: {\n ...(path === undefined ? {} : { path }),\n ...(command === undefined ? {} : { command }),\n ...(offsetLimited === undefined ? {} : { offsetLimited }),\n },\n };\n}\n\nconst ALLOW: GuardDecision = { action: 'allow' };\n\n/**\n * The decision, pure given a stat function — so tests exercise every branch without\n * a filesystem, and the script wires `statSync` in.\n *\n * The shape of the rules, from the research note (§ 5, \"the ~8 KB threshold,\n * validated\", with both amendments):\n *\n * - **Read**: stat the exact target; deny only above the threshold, and never when\n * the read is already windowed (`offsetLimited`) — a windowed read of a huge file\n * is an economy move.\n * - **Bash**: only a *simple* command whose subject is a statable named file can be\n * judged pre-run. `cat <file>` above the threshold is denied with the faithful\n * replacement; `grep`/`rg` output is unknowable pre-run, so it passes in deny\n * mode and is wrapped (` | smelt --budget … --focus <pattern>`) only under\n * `hooks.enforcement: \"rewrite\"`. Pipelines, redirects, substitutions — anything\n * this parser cannot be sure about — pass untouched. Fail open, always.\n */\nexport function decide(\n request: GuardRequest,\n settings: GuardSettings,\n cwd: string,\n statFile: (path: string) => { size: number; isFile: boolean } | undefined = statFileReal,\n): GuardDecision {\n if (request.tool === 'Read') {\n return decideRead(request.input, settings, cwd, statFile);\n }\n if (request.tool === 'Bash') {\n return decideBash(request.input, settings, cwd, statFile);\n }\n return ALLOW;\n}\n\nfunction statFileReal(path: string): { size: number; isFile: boolean } | undefined {\n try {\n const stat = statSync(path);\n return { size: stat.size, isFile: stat.isFile() };\n } catch {\n return undefined;\n }\n}\n\nfunction decideRead(\n input: GuardRequest['input'],\n settings: GuardSettings,\n cwd: string,\n statFile: (path: string) => { size: number; isFile: boolean } | undefined,\n): GuardDecision {\n if (input.path === undefined) return ALLOW;\n if (input.offsetLimited === true) return ALLOW; // already windowed — an economy move\n const path = isAbsolute(input.path) ? input.path : resolve(cwd, input.path);\n const stat = statFile(path);\n if (stat === undefined || !stat.isFile) return ALLOW; // let the tool surface its own error\n if (stat.size <= settings.thresholdBytes) return ALLOW;\n return denyOversized(path, stat.size, settings, 'Reading it raw');\n}\n\nfunction decideBash(\n input: GuardRequest['input'],\n settings: GuardSettings,\n cwd: string,\n statFile: (path: string) => { size: number; isFile: boolean } | undefined,\n): GuardDecision {\n if (input.command === undefined) return ALLOW;\n const command = input.command.trim();\n // A command already using smelt is the model doing the right thing — including the\n // exact replacement a previous deny suggested. Never intercept it (and never wrap a\n // wrapped pipeline a second time).\n if (/(^|[\\s/\"'=])smelt($|[\\s\"'])/.test(command)) return ALLOW;\n\n const words = simpleCommandWords(command);\n if (words === undefined || words.length === 0) return ALLOW; // not simple — unknowable pre-run\n\n const program = words[0]!.split('/').at(-1)!;\n\n if (program === 'cat') {\n const files = words.slice(1).filter((word) => word !== '--' && !word.startsWith('-'));\n for (const file of files) {\n const path = isAbsolute(file) ? file : resolve(cwd, file);\n const stat = statFile(path);\n if (stat === undefined || !stat.isFile) continue;\n if (stat.size > settings.thresholdBytes) {\n const decision = denyOversized(path, stat.size, settings, `\\`${command}\\``);\n // The suggestion is only a *faithful* replacement when cat named exactly this\n // one file; `cat a b` replaced by `smelt a` would silently drop b, so the\n // multi-file case keeps the reason (the model decides), drops the suggestion\n // (nothing may auto-substitute it), and says out loud that the named\n // replacement covers only the oversized file — a model following the reason\n // verbatim must not silently drop the others.\n return files.length === 1\n ? decision\n : {\n action: 'deny',\n reason:\n `${decision.reason ?? ''} Note: \\`${command}\\` names ` +\n `${String(files.length)} files and the replacement above covers only ` +\n `${path} — read the other file(s) separately (cat is fine for the ones ` +\n `under the threshold).`,\n };\n }\n }\n return ALLOW;\n }\n\n if ((program === 'grep' || program === 'rg') && settings.enforcement === 'rewrite') {\n const pattern = searchPattern(words);\n if (pattern === undefined) return ALLOW;\n // `--focus` on the wrap only where it distinguishes lines: a plain grep's every\n // output line contains the searched pattern, so focusing on it would protect the\n // entire output — zero elisions exactly when the output is large, plus an\n // over-budget exit — and the wrap lets the lexical planner keep the head and tail\n // and collapse the middle instead. A search with context prints non-matching\n // lines too, and there the pattern the guard already parsed is exactly the focus.\n // One derivation, `focusTermsFor`, states which is which; the same function\n // resolves a `--producer` hint in the ops seam, so both doors agree with this wrap.\n const focus = focusTermsFor(command);\n const focused = focus.map((term) => ` --focus ${shellQuote(term)}`).join('');\n const wrapped = `${command} | ${SMELT_CLI} --budget ${String(settings.budgetBytes)}${focused}`;\n return {\n action: 'deny',\n reason:\n `smelt guard (rewrite mode): \\`${program}\\` output size is unknowable before it runs, ` +\n `so pipe it through smelt instead. Run exactly: ${wrapped} — output within the ` +\n `budget passes through untouched; past it, elided regions leave <<smelt/v1 …>> ` +\n `markers${focused === '' ? '' : `, and the${focused} keeps every match and its context verbatim`}. ` +\n `${retrieveSentence(settings)}`,\n suggestion: wrapped,\n };\n }\n\n return ALLOW;\n}\n\n/**\n * The one sentence about getting elided bytes back — honest about the store: the\n * retrieval promise is only made when a persistent store is configured, because a\n * memory store dies with the process and `retrieve` then refuses (`resolveStoreRun`).\n */\nfunction retrieveSentence(settings: GuardSettings): string {\n return settings.persistentStore\n ? `\\`${SMELT_CLI} retrieve <hash>\\` prints any marker's bytes back, byte for byte.`\n : `\\`${SMELT_CLI} retrieve <hash>\\` can print a marker's bytes back once a persistent ` +\n `store is configured ({\"store\":{\"kind\":\"directory\",\"path\":…}} in smelt.config.json — ` +\n `\\`smelt hooks install\\` writes one); without it the elided bytes die with the ` +\n `smelt process.`;\n}\n\n/** The deny everything above the threshold gets: steering text plus the exact command. */\nfunction denyOversized(\n path: string,\n size: number,\n settings: GuardSettings,\n what: string,\n): GuardDecision {\n const replacement = `${SMELT_CLI} ${shellQuote(path)} --budget ${String(settings.budgetBytes)}`;\n return {\n action: 'deny',\n reason:\n `smelt guard: ${path} is ${String(size)} bytes — over the ${String(settings.thresholdBytes)}-byte ` +\n `threshold (smelt.config.json hooks.thresholdBytes). ${what} would spend context on bytes ` +\n `the task may not need. Run instead: ${replacement} --focus <what you are looking for> ` +\n `(repeat --focus per term; focused regions survive verbatim). Elided regions leave ` +\n `<<smelt/v1 …>> markers — ${retrieveSentence(settings)} A windowed read (offset/limit) ` +\n `of just the lines you need is also fine.`,\n suggestion: replacement,\n };\n}\n\n/* ------------------------------------------------------------------------------------\n * Process plumbing, for the shims that run as one\n * ---------------------------------------------------------------------------------- */\n\n/** True when this module is the file node was asked to run, not an import. */\nexport function isMainModule(moduleUrl: string): boolean {\n const entry = process.argv[1];\n if (entry === undefined) return false;\n try {\n return pathToFileURL(entry).href === moduleUrl;\n } catch {\n return false;\n }\n}\n\n/** Every byte of fd 0 to EOF, retrying EAGAIN — the same shape `cli/bin.ts` uses. */\nexport function readAllOfStdin(): string {\n const sleeper = new Int32Array(new SharedArrayBuffer(4));\n const chunks: Buffer[] = [];\n const chunk = Buffer.alloc(1 << 16);\n for (;;) {\n let bytesRead: number;\n try {\n bytesRead = readSync(0, chunk, 0, chunk.length, null);\n } catch (error) {\n const code = (error as { code?: string }).code;\n if (code === 'EAGAIN') {\n Atomics.wait(sleeper, 0, 0, 10);\n continue;\n }\n if (code === 'EOF') break;\n throw error;\n }\n if (bytesRead === 0) break;\n chunks.push(Buffer.from(chunk.subarray(0, bytesRead)));\n }\n return Buffer.concat(chunks).toString('utf8');\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export { DEFAULT_STRATEGY, isStrategy, PLANNERS, STRATEGIES } from './plan/plann
|
|
|
24
24
|
export type { PlannerFactoryOptions } from './plan/planners.ts';
|
|
25
25
|
export { isStructuralLanguage, planStructural, STRUCTURAL_LANGUAGES, STRUCTURAL_PLANNER_ID, StructuralPlanner, } from './plan/structural.ts';
|
|
26
26
|
export type { StructuralLanguage, StructuralPlannerOptions } from './plan/structural.ts';
|
|
27
|
-
export { createRetrieveTool, RETRIEVE_TOOL_NAME } from './retrieve.ts';
|
|
27
|
+
export { createRetrieveBatchTool, createRetrieveTool, RETRIEVE_BATCH_TOOL_NAME, RETRIEVE_TOOL_NAME, } from './retrieve.ts';
|
|
28
28
|
export { unconfiguredDistillStage, unconfiguredRerankStage } from './stages.ts';
|
|
29
29
|
export { MemoryElisionStore } from './store.ts';
|
|
30
30
|
export { DIRECTORY_STORE_FORMAT, DIRECTORY_STORE_VERSION, DirectoryElisionStore, } from './store-dir.ts';
|
|
@@ -52,7 +52,7 @@ export { runSetup } from './cli/setup.ts';
|
|
|
52
52
|
export type { SetupCheck, SetupFileAction, SetupIo, SetupOptions, SetupReceipt, } from './cli/setup.ts';
|
|
53
53
|
export { runDoctor } from './cli/doctor.ts';
|
|
54
54
|
export type { DoctorBlock, DoctorConfig, DoctorIo, DoctorMcp, DoctorOptions, DoctorReceipt, } from './cli/doctor.ts';
|
|
55
|
-
export { retrieveStats } from './stats.ts';
|
|
55
|
+
export { retrieveStats, ruleLedger } from './stats.ts';
|
|
56
56
|
export type { RawRetrieveCounters } from './stats.ts';
|
|
57
57
|
/**
|
|
58
58
|
* The SetupRecipe: the one true way to put smelt on a machine, as data. Public for the
|
|
@@ -87,8 +87,8 @@ export type { ResolvedMapRun } from './cli/subcommands/map.ts';
|
|
|
87
87
|
* `smelt` CLI and the MCP tools run the same middle instead of two copies of it. See
|
|
88
88
|
* `src/ops/index.ts` for what belongs here and what stays in an adapter.
|
|
89
89
|
*/
|
|
90
|
-
export { budgetFault, budgetMalformed, budgetRequired, mapTree, openStore, readBlob, readCounters, readTree, resolveStrategy, retrieveBytes, smeltBlob, } from './ops/index.ts';
|
|
91
|
-
export type { BudgetFault, BudgetNaming, MapTreeOp, ReadCountersOp, ResolvedStrategy, RetrieveBytesOp, Ruling, SmeltBlobOp, SmeltBlobOutcome, StrategySource, TreeNaming, } from './ops/index.ts';
|
|
90
|
+
export { budgetFault, budgetMalformed, budgetRequired, mapTree, openStore, readBlob, readCounters, readLedger, readTree, resolveStrategy, retrieveBytes, retrieveMany, smeltBlob, } from './ops/index.ts';
|
|
91
|
+
export type { BudgetFault, BudgetNaming, FocusSource, MapTreeOp, ReadCountersOp, ReadLedgerOp, ResolvedFocus, ResolvedStrategy, RetrieveBytesOp, RetrieveManyOp, Ruling, SmeltBlobOp, SmeltBlobOutcome, StrategySource, TreeNaming, } from './ops/index.ts';
|
|
92
92
|
/**
|
|
93
93
|
* Which planner a smelter uses, named by string. The names, their factories, and this
|
|
94
94
|
* type all come from the one {@link PLANNERS} registry in `src/plan/planners.ts`, so
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1E;;;;;;;;GAQG;AAEH,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,CAAC;AACxD,OAAO,EACL,SAAS,EACT,aAAa,EACb,qBAAqB,EACrB,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClE,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,sBAAsB,EACtB,kBAAkB,GACnB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAClG,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxE,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACpF,YAAY,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACxF,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AACnE,cAAc,YAAY,CAAC;AAC3B,OAAO,EACL,eAAe,EACf,QAAQ,EACR,QAAQ,EACR,IAAI,EACJ,YAAY,EACZ,cAAc,EACd,MAAM,GACP,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC1F,OAAO,EACL,4BAA4B,EAC5B,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,UAAU,GACX,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,2BAA2B,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC5E,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,2BAA2B,EAC3B,WAAW,EACX,uBAAuB,EACvB,oBAAoB,EACpB,0BAA0B,GAC3B,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,OAAO,EACP,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC3F,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACzF,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,WAAW,EACX,YAAY,EACZ,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACpG,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,OAAO,GACR,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,YAAY,EACV,UAAU,EACV,eAAe,EACf,OAAO,EACP,YAAY,EACZ,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,YAAY,EACV,WAAW,EACX,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,aAAa,EACb,aAAa,GACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,YAAY,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC9D,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EACL,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC1F;;;;;GAKG;AACH,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAC9F,YAAY,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACjF,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,YAAY,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACnF,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACrF,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE/D;;;;;GAKG;AACH,OAAO,EACL,WAAW,EACX,eAAe,EACf,cAAc,EACd,OAAO,EACP,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,QAAQ,EACR,eAAe,EACf,aAAa,EACb,SAAS,GACV,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,WAAW,EACX,YAAY,EACZ,SAAS,EACT,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,MAAM,EACN,WAAW,EACX,gBAAgB,EAChB,cAAc,EACd,UAAU,GACX,MAAM,gBAAgB,CAAC;AAExB;;;;GAIG;AACH,YAAY,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC","sourcesContent":["import type { ApplyOptions, MarkerBuilder, MarkerInfo } from './apply.ts';\n\n/**\n * The public surface: a barrel over the modules that hold the reasoning.\n *\n * `createSmelter()` itself lives in `./smelter.ts` so that nothing inside `src/` has\n * to import this file to build a smelter — the CLI's default verb does exactly that,\n * and a barrel that imports the CLI which imports the barrel is a cycle whose only\n * symptom is a registry evaluating to `undefined` in whichever module the loader\n * entered first. Every name is re-exported here, so consumers see no difference.\n */\n\nexport type { ApplyOptions, MarkerBuilder, MarkerInfo };\nexport {\n applyPlan,\n defaultMarker,\n MARKER_FORMAT_VERSION,\n markerPricing,\n reconstruct,\n} from './apply.ts';\nexport { detectLanguage, SUPPORTED_LANGUAGES } from './detect.ts';\nexport * from './errors.ts';\nexport { contentHash, HASH_LENGTH } from './hash.ts';\nexport {\n ALLOWED_NODE_BUILTINS,\n ALLOWED_PACKAGES,\n ALLOWED_URL_SCHEMES,\n assertLocalResource,\n FORBIDDEN_GLOBALS,\n FORBIDDEN_NODE_MODULES,\n FORBIDDEN_PACKAGES,\n} from './net/policy.ts';\nexport type { LocalResource } from './net/policy.ts';\nexport { clearGrammarCache, grammarPath, loadGrammar, WASM_BY_LANGUAGE } from './plan/grammar.ts';\nexport { AUTO_PLANNER_ID, AutoPlanner, planAuto } from './plan/auto.ts';\nexport type { AutoPlannerOptions } from './plan/auto.ts';\nexport { LEXICAL_PLANNER_ID, LexicalPlanner, planLexical } from './plan/lexical.ts';\nexport type { LexicalPlannerOptions } from './plan/lexical.ts';\nexport { DEFAULT_STRATEGY, isStrategy, PLANNERS, STRATEGIES } from './plan/planners.ts';\nexport type { PlannerFactoryOptions } from './plan/planners.ts';\nexport {\n isStructuralLanguage,\n planStructural,\n STRUCTURAL_LANGUAGES,\n STRUCTURAL_PLANNER_ID,\n StructuralPlanner,\n} from './plan/structural.ts';\nexport type { StructuralLanguage, StructuralPlannerOptions } from './plan/structural.ts';\nexport { createRetrieveTool, RETRIEVE_TOOL_NAME } from './retrieve.ts';\nexport { unconfiguredDistillStage, unconfiguredRerankStage } from './stages.ts';\nexport { MemoryElisionStore } from './store.ts';\nexport {\n DIRECTORY_STORE_FORMAT,\n DIRECTORY_STORE_VERSION,\n DirectoryElisionStore,\n} from './store-dir.ts';\nexport type { DirectoryElisionStoreOptions } from './store-dir.ts';\nexport * from './types.ts';\nexport {\n CLI_JSON_FORMAT,\n CLI_NAME,\n cliUsage,\n EXIT,\n formatReport,\n parseSmeltArgs,\n runCli,\n} from './cli/run.ts';\nexport type { AnswerStream, CliIo, CliJsonEnvelope, SmeltInvocation } from './cli/run.ts';\nexport {\n ANTHROPIC_PROMPT_CACHE_FACTS,\n CACHE_BREAKER_RULES,\n detectCacheBreakers,\n findPrefixDivergence,\n} from './cache/prefix.ts';\nexport type {\n CacheWarning,\n PrefixDivergence,\n PromptStructure,\n PromptTool,\n} from './cache/prefix.ts';\nexport { MARKER_LINE_COMMENT_LEADERS, markerForLanguage } from './apply.ts';\nexport {\n buildRepoMap,\n DEFAULT_REPO_IGNORE,\n REPO_MAP_CACHE_CORRUPT_RULE,\n REPO_MAP_ID,\n REPO_MAP_PATH_ONLY_RULE,\n REPO_MAP_RANKED_RULE,\n REPO_MAP_UNREFERENCED_RULE,\n} from './repomap/map.ts';\nexport type {\n RepoMap,\n RepoMapCacheCounts,\n RepoMapEntry,\n RepoMapOptions,\n RepoMapPathEntry,\n RepoMapReason,\n RepoMapWarning,\n} from './repomap/map.ts';\nexport { PAGERANK_DAMPING, PAGERANK_ITERATIONS, rankDefinitions } from './repomap/rank.ts';\nexport type { FileTagsEntry, RankedDefinition } from './repomap/rank.ts';\nexport { extractTags } from './repomap/tags.ts';\nexport type { DefinitionTag, FileTags, ReferenceTag } from './repomap/tags.ts';\nexport { TAGS_CACHE_FORMAT, TAGS_CACHE_VERSION, tagsCacheKey } from './repomap/cache.ts';\nexport { nodeFsReader } from './repomap/reader.ts';\nexport type { DirEntry, FileStat, RepoReader } from './repomap/reader.ts';\nexport {\n CONFIG_FILE_NAME,\n CONFIG_VERSION,\n configuredStore,\n findConfigFile,\n loadNearestConfig,\n parseConfig,\n renderConfig,\n resolveStorePath,\n} from './cli/config.ts';\nexport type { ConfiguredStore, LoadedConfig, SmeltConfig, SmeltConfigStore } from './cli/config.ts';\nexport {\n MEASURE_STUB_FILE,\n measureStubSource,\n RERANK_STUB_FILE,\n rerankStubSource,\n runInit,\n} from './cli/init.ts';\nexport type { InitIo } from './cli/init.ts';\nexport { runSetup } from './cli/setup.ts';\nexport type {\n SetupCheck,\n SetupFileAction,\n SetupIo,\n SetupOptions,\n SetupReceipt,\n} from './cli/setup.ts';\nexport { runDoctor } from './cli/doctor.ts';\nexport type {\n DoctorBlock,\n DoctorConfig,\n DoctorIo,\n DoctorMcp,\n DoctorOptions,\n DoctorReceipt,\n} from './cli/doctor.ts';\nexport { retrieveStats } from './stats.ts';\nexport type { RawRetrieveCounters } from './stats.ts';\n\n/**\n * The SetupRecipe: the one true way to put smelt on a machine, as data. Public for the\n * same reason the harness views are — something outside this package renders it (the\n * site's fact generator), and the docs and skill are pinned to it rather than retyping\n * it. See `src/setup/recipe.ts`.\n */\nexport { SETUP_RECIPE, SETUP_STEPS } from './setup/recipe.ts';\nexport type { SetupRecipe, SetupStep } from './setup/recipe.ts';\nexport {\n LANGUAGE_PROFILES,\n profileFor,\n profileForPath,\n structuralLanguages,\n} from './lang/registry.ts';\nexport type { LanguageProfile, LanguageStructure, RepoMapFacts } from './lang/profile.ts';\n/**\n * The harness registry's rendered views. Public for the same reason the ops seam is:\n * something outside this package renders them — the site's `facts.json` generator —\n * and the alternative is a second copy of the tier table typed into a React component,\n * which is exactly the drift `harnessesByTier()` exists to end.\n */\nexport { harnessesByTier, harnessNames, HARNESSES, HARNESS_IDS } from './harness/registry.ts';\nexport type { HarnessTierGroup } from './harness/registry.ts';\nexport { harnessLabel, HARNESS_TIERS, TIER_HONESTY } from './harness/profile.ts';\nexport type { HarnessId, HarnessTier } from './harness/profile.ts';\nexport { resolveRun } from './cli/subcommands/smelt.ts';\nexport type { ResolvedRun } from './cli/subcommands/smelt.ts';\nexport { REPO_MAP_FOCUS_RULE } from './repomap/map.ts';\nexport { CLI_MAP_JSON_FORMAT, formatMapReport, resolveMapRun } from './cli/run.ts';\nexport type { CliInvocation, CliMapJsonEnvelope, MapInvocation } from './cli/run.ts';\nexport type { MapReportInput } from './cli/report.ts';\nexport type { ResolvedMapRun } from './cli/subcommands/map.ts';\n\n/**\n * The operations seam — the four verbs and the laws their inputs must satisfy, below\n * every front door. `@smeltjs/mcp` consumes these as an ordinary dependency, so the\n * `smelt` CLI and the MCP tools run the same middle instead of two copies of it. See\n * `src/ops/index.ts` for what belongs here and what stays in an adapter.\n */\nexport {\n budgetFault,\n budgetMalformed,\n budgetRequired,\n mapTree,\n openStore,\n readBlob,\n readCounters,\n readTree,\n resolveStrategy,\n retrieveBytes,\n smeltBlob,\n} from './ops/index.ts';\nexport type {\n BudgetFault,\n BudgetNaming,\n MapTreeOp,\n ReadCountersOp,\n ResolvedStrategy,\n RetrieveBytesOp,\n Ruling,\n SmeltBlobOp,\n SmeltBlobOutcome,\n StrategySource,\n TreeNaming,\n} from './ops/index.ts';\n\n/**\n * Which planner a smelter uses, named by string. The names, their factories, and this\n * type all come from the one {@link PLANNERS} registry in `src/plan/planners.ts`, so\n * the CLI's validation and help text cannot drift from what `createSmelter` builds.\n */\nexport type { Strategy } from './plan/planners.ts';\n\nexport { createSmelter } from './smelter.ts';\nexport type { Smelter, SmelterConfig, SmeltCallOptions } from './smelter.ts';\n"]}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ApplyOptions, MarkerBuilder, MarkerInfo } from './apply.ts';\n\n/**\n * The public surface: a barrel over the modules that hold the reasoning.\n *\n * `createSmelter()` itself lives in `./smelter.ts` so that nothing inside `src/` has\n * to import this file to build a smelter — the CLI's default verb does exactly that,\n * and a barrel that imports the CLI which imports the barrel is a cycle whose only\n * symptom is a registry evaluating to `undefined` in whichever module the loader\n * entered first. Every name is re-exported here, so consumers see no difference.\n */\n\nexport type { ApplyOptions, MarkerBuilder, MarkerInfo };\nexport {\n applyPlan,\n defaultMarker,\n MARKER_FORMAT_VERSION,\n markerPricing,\n reconstruct,\n} from './apply.ts';\nexport { detectLanguage, SUPPORTED_LANGUAGES } from './detect.ts';\nexport * from './errors.ts';\nexport { contentHash, HASH_LENGTH } from './hash.ts';\nexport {\n ALLOWED_NODE_BUILTINS,\n ALLOWED_PACKAGES,\n ALLOWED_URL_SCHEMES,\n assertLocalResource,\n FORBIDDEN_GLOBALS,\n FORBIDDEN_NODE_MODULES,\n FORBIDDEN_PACKAGES,\n} from './net/policy.ts';\nexport type { LocalResource } from './net/policy.ts';\nexport { clearGrammarCache, grammarPath, loadGrammar, WASM_BY_LANGUAGE } from './plan/grammar.ts';\nexport { AUTO_PLANNER_ID, AutoPlanner, planAuto } from './plan/auto.ts';\nexport type { AutoPlannerOptions } from './plan/auto.ts';\nexport { LEXICAL_PLANNER_ID, LexicalPlanner, planLexical } from './plan/lexical.ts';\nexport type { LexicalPlannerOptions } from './plan/lexical.ts';\nexport { DEFAULT_STRATEGY, isStrategy, PLANNERS, STRATEGIES } from './plan/planners.ts';\nexport type { PlannerFactoryOptions } from './plan/planners.ts';\nexport {\n isStructuralLanguage,\n planStructural,\n STRUCTURAL_LANGUAGES,\n STRUCTURAL_PLANNER_ID,\n StructuralPlanner,\n} from './plan/structural.ts';\nexport type { StructuralLanguage, StructuralPlannerOptions } from './plan/structural.ts';\nexport {\n createRetrieveBatchTool,\n createRetrieveTool,\n RETRIEVE_BATCH_TOOL_NAME,\n RETRIEVE_TOOL_NAME,\n} from './retrieve.ts';\nexport { unconfiguredDistillStage, unconfiguredRerankStage } from './stages.ts';\nexport { MemoryElisionStore } from './store.ts';\nexport {\n DIRECTORY_STORE_FORMAT,\n DIRECTORY_STORE_VERSION,\n DirectoryElisionStore,\n} from './store-dir.ts';\nexport type { DirectoryElisionStoreOptions } from './store-dir.ts';\nexport * from './types.ts';\nexport {\n CLI_JSON_FORMAT,\n CLI_NAME,\n cliUsage,\n EXIT,\n formatReport,\n parseSmeltArgs,\n runCli,\n} from './cli/run.ts';\nexport type { AnswerStream, CliIo, CliJsonEnvelope, SmeltInvocation } from './cli/run.ts';\nexport {\n ANTHROPIC_PROMPT_CACHE_FACTS,\n CACHE_BREAKER_RULES,\n detectCacheBreakers,\n findPrefixDivergence,\n} from './cache/prefix.ts';\nexport type {\n CacheWarning,\n PrefixDivergence,\n PromptStructure,\n PromptTool,\n} from './cache/prefix.ts';\nexport { MARKER_LINE_COMMENT_LEADERS, markerForLanguage } from './apply.ts';\nexport {\n buildRepoMap,\n DEFAULT_REPO_IGNORE,\n REPO_MAP_CACHE_CORRUPT_RULE,\n REPO_MAP_ID,\n REPO_MAP_PATH_ONLY_RULE,\n REPO_MAP_RANKED_RULE,\n REPO_MAP_UNREFERENCED_RULE,\n} from './repomap/map.ts';\nexport type {\n RepoMap,\n RepoMapCacheCounts,\n RepoMapEntry,\n RepoMapOptions,\n RepoMapPathEntry,\n RepoMapReason,\n RepoMapWarning,\n} from './repomap/map.ts';\nexport { PAGERANK_DAMPING, PAGERANK_ITERATIONS, rankDefinitions } from './repomap/rank.ts';\nexport type { FileTagsEntry, RankedDefinition } from './repomap/rank.ts';\nexport { extractTags } from './repomap/tags.ts';\nexport type { DefinitionTag, FileTags, ReferenceTag } from './repomap/tags.ts';\nexport { TAGS_CACHE_FORMAT, TAGS_CACHE_VERSION, tagsCacheKey } from './repomap/cache.ts';\nexport { nodeFsReader } from './repomap/reader.ts';\nexport type { DirEntry, FileStat, RepoReader } from './repomap/reader.ts';\nexport {\n CONFIG_FILE_NAME,\n CONFIG_VERSION,\n configuredStore,\n findConfigFile,\n loadNearestConfig,\n parseConfig,\n renderConfig,\n resolveStorePath,\n} from './cli/config.ts';\nexport type { ConfiguredStore, LoadedConfig, SmeltConfig, SmeltConfigStore } from './cli/config.ts';\nexport {\n MEASURE_STUB_FILE,\n measureStubSource,\n RERANK_STUB_FILE,\n rerankStubSource,\n runInit,\n} from './cli/init.ts';\nexport type { InitIo } from './cli/init.ts';\nexport { runSetup } from './cli/setup.ts';\nexport type {\n SetupCheck,\n SetupFileAction,\n SetupIo,\n SetupOptions,\n SetupReceipt,\n} from './cli/setup.ts';\nexport { runDoctor } from './cli/doctor.ts';\nexport type {\n DoctorBlock,\n DoctorConfig,\n DoctorIo,\n DoctorMcp,\n DoctorOptions,\n DoctorReceipt,\n} from './cli/doctor.ts';\nexport { retrieveStats, ruleLedger } from './stats.ts';\nexport type { RawRetrieveCounters } from './stats.ts';\n\n/**\n * The SetupRecipe: the one true way to put smelt on a machine, as data. Public for the\n * same reason the harness views are — something outside this package renders it (the\n * site's fact generator), and the docs and skill are pinned to it rather than retyping\n * it. See `src/setup/recipe.ts`.\n */\nexport { SETUP_RECIPE, SETUP_STEPS } from './setup/recipe.ts';\nexport type { SetupRecipe, SetupStep } from './setup/recipe.ts';\nexport {\n LANGUAGE_PROFILES,\n profileFor,\n profileForPath,\n structuralLanguages,\n} from './lang/registry.ts';\nexport type { LanguageProfile, LanguageStructure, RepoMapFacts } from './lang/profile.ts';\n/**\n * The harness registry's rendered views. Public for the same reason the ops seam is:\n * something outside this package renders them — the site's `facts.json` generator —\n * and the alternative is a second copy of the tier table typed into a React component,\n * which is exactly the drift `harnessesByTier()` exists to end.\n */\nexport { harnessesByTier, harnessNames, HARNESSES, HARNESS_IDS } from './harness/registry.ts';\nexport type { HarnessTierGroup } from './harness/registry.ts';\nexport { harnessLabel, HARNESS_TIERS, TIER_HONESTY } from './harness/profile.ts';\nexport type { HarnessId, HarnessTier } from './harness/profile.ts';\nexport { resolveRun } from './cli/subcommands/smelt.ts';\nexport type { ResolvedRun } from './cli/subcommands/smelt.ts';\nexport { REPO_MAP_FOCUS_RULE } from './repomap/map.ts';\nexport { CLI_MAP_JSON_FORMAT, formatMapReport, resolveMapRun } from './cli/run.ts';\nexport type { CliInvocation, CliMapJsonEnvelope, MapInvocation } from './cli/run.ts';\nexport type { MapReportInput } from './cli/report.ts';\nexport type { ResolvedMapRun } from './cli/subcommands/map.ts';\n\n/**\n * The operations seam — the four verbs and the laws their inputs must satisfy, below\n * every front door. `@smeltjs/mcp` consumes these as an ordinary dependency, so the\n * `smelt` CLI and the MCP tools run the same middle instead of two copies of it. See\n * `src/ops/index.ts` for what belongs here and what stays in an adapter.\n */\nexport {\n budgetFault,\n budgetMalformed,\n budgetRequired,\n mapTree,\n openStore,\n readBlob,\n readCounters,\n readLedger,\n readTree,\n resolveStrategy,\n retrieveBytes,\n retrieveMany,\n smeltBlob,\n} from './ops/index.ts';\nexport type {\n BudgetFault,\n BudgetNaming,\n FocusSource,\n MapTreeOp,\n ReadCountersOp,\n ReadLedgerOp,\n ResolvedFocus,\n ResolvedStrategy,\n RetrieveBytesOp,\n RetrieveManyOp,\n Ruling,\n SmeltBlobOp,\n SmeltBlobOutcome,\n StrategySource,\n TreeNaming,\n} from './ops/index.ts';\n\n/**\n * Which planner a smelter uses, named by string. The names, their factories, and this\n * type all come from the one {@link PLANNERS} registry in `src/plan/planners.ts`, so\n * the CLI's validation and help text cannot drift from what `createSmelter` builds.\n */\nexport type { Strategy } from './plan/planners.ts';\n\nexport { createSmelter } from './smelter.ts';\nexport type { Smelter, SmelterConfig, SmeltCallOptions } from './smelter.ts';\n"]}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ export { AUTO_PLANNER_ID, AutoPlanner, planAuto } from './plan/auto.js';
|
|
|
8
8
|
export { LEXICAL_PLANNER_ID, LexicalPlanner, planLexical } from './plan/lexical.js';
|
|
9
9
|
export { DEFAULT_STRATEGY, isStrategy, PLANNERS, STRATEGIES } from './plan/planners.js';
|
|
10
10
|
export { isStructuralLanguage, planStructural, STRUCTURAL_LANGUAGES, STRUCTURAL_PLANNER_ID, StructuralPlanner, } from './plan/structural.js';
|
|
11
|
-
export { createRetrieveTool, RETRIEVE_TOOL_NAME } from './retrieve.js';
|
|
11
|
+
export { createRetrieveBatchTool, createRetrieveTool, RETRIEVE_BATCH_TOOL_NAME, RETRIEVE_TOOL_NAME, } from './retrieve.js';
|
|
12
12
|
export { unconfiguredDistillStage, unconfiguredRerankStage } from './stages.js';
|
|
13
13
|
export { MemoryElisionStore } from './store.js';
|
|
14
14
|
export { DIRECTORY_STORE_FORMAT, DIRECTORY_STORE_VERSION, DirectoryElisionStore, } from './store-dir.js';
|
|
@@ -25,7 +25,7 @@ export { CONFIG_FILE_NAME, CONFIG_VERSION, configuredStore, findConfigFile, load
|
|
|
25
25
|
export { MEASURE_STUB_FILE, measureStubSource, RERANK_STUB_FILE, rerankStubSource, runInit, } from './cli/init.js';
|
|
26
26
|
export { runSetup } from './cli/setup.js';
|
|
27
27
|
export { runDoctor } from './cli/doctor.js';
|
|
28
|
-
export { retrieveStats } from './stats.js';
|
|
28
|
+
export { retrieveStats, ruleLedger } from './stats.js';
|
|
29
29
|
/**
|
|
30
30
|
* The SetupRecipe: the one true way to put smelt on a machine, as data. Public for the
|
|
31
31
|
* same reason the harness views are — something outside this package renders it (the
|
|
@@ -51,6 +51,6 @@ export { CLI_MAP_JSON_FORMAT, formatMapReport, resolveMapRun } from './cli/run.j
|
|
|
51
51
|
* `smelt` CLI and the MCP tools run the same middle instead of two copies of it. See
|
|
52
52
|
* `src/ops/index.ts` for what belongs here and what stays in an adapter.
|
|
53
53
|
*/
|
|
54
|
-
export { budgetFault, budgetMalformed, budgetRequired, mapTree, openStore, readBlob, readCounters, readTree, resolveStrategy, retrieveBytes, smeltBlob, } from './ops/index.js';
|
|
54
|
+
export { budgetFault, budgetMalformed, budgetRequired, mapTree, openStore, readBlob, readCounters, readLedger, readTree, resolveStrategy, retrieveBytes, retrieveMany, smeltBlob, } from './ops/index.js';
|
|
55
55
|
export { createSmelter } from './smelter.js';
|
|
56
56
|
//# sourceMappingURL=index.js.map
|