@intentic/claude-plugin 1.331.0 → 1.333.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/stats.mjs CHANGED
@@ -13,8 +13,6 @@ var OPENING_SEARCHES = { name: "openingSearches", of: (turn) => turn.openingSear
13
13
  var ROOT_LISTINGS = { name: "openingListings", of: (turn) => turn.openingListings, round: round1 };
14
14
  var CALLS_BEFORE_TARGET = { name: "callsBeforeTarget", of: (turn) => turn.callsBeforeTarget, round: round1 };
15
15
  var FAILED_CALLS = { name: "failedCalls", of: (turn) => turn.failedCalls, round: round1 };
16
- var ROUND_TRIPS = { name: "roundTrips", of: (turn) => turn.roundTrips, round: round1 };
17
- var CONTEXT_PER_CALL = { name: "contextPerCall", of: (turn) => turn.contextPerCall, round: Math.round };
18
16
  var armOfValues = (values) => {
19
17
  if (values.length === 0) {
20
18
  return { turns: 0, mean: 0, variance: 0 };
@@ -74,10 +72,7 @@ var MECHANISMS = {
74
72
  // The map is sent once and judged on the turn it was sent to; `callsBeforeTarget` exists only on turns that edited.
75
73
  map: { metrics: [ROOT_LISTINGS, CALLS_BEFORE_TARGET], sampleUnit: "opening turns", sample: openingTurn },
76
74
  // The brief rides the whole session, so turn 40 is as much evidence as turn 1.
77
- notes: { metrics: [FAILED_CALLS, CALLS_BEFORE_TARGET], sampleUnit: "conversations", sample: meanOfTurns },
78
- // Judged on what it is for, the prompt each call carries, and on what it could cost: a model that has lost a result
79
- // it needed runs the tool again (more round trips) or guesses (more calls that fail).
80
- clearing: { metrics: [CONTEXT_PER_CALL, ROUND_TRIPS, FAILED_CALLS], sampleUnit: "conversations", sample: meanOfTurns }
75
+ notes: { metrics: [FAILED_CALLS, CALLS_BEFORE_TARGET], sampleUnit: "conversations", sample: meanOfTurns }
81
76
  };
82
77
  var measureExperiment = (turns, design) => {
83
78
  const latest = turns.filter((turn) => design.arm(turn) !== void 0 && design.cohort(turn) !== void 0).toSorted((left, right) => right.at - left.at)[0];
@@ -122,6 +117,17 @@ var measureExperiment = (turns, design) => {
122
117
  import { readdirSync, readFileSync } from "node:fs";
123
118
  import { basename, dirname, join } from "node:path";
124
119
 
120
+ // ../../_tools/base/src/errors.ts
121
+ var errorMessage = (error) => error instanceof Error ? error.message : String(error);
122
+ var errnoCode = (error) => {
123
+ const code = typeof error === `object` && error !== null ? error.code : void 0;
124
+ return typeof code === `string` ? code : void 0;
125
+ };
126
+ var isMissing = (error) => {
127
+ const code = errnoCode(error);
128
+ return code === `ENOENT` || code === `ENOTDIR`;
129
+ };
130
+
125
131
  // ../../_search/iq/src/recall/transcript/lines.ts
126
132
  var asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
127
133
  var asString = (value) => typeof value === "string" ? value : void 0;
@@ -383,10 +389,6 @@ var parse = (raw) => {
383
389
  return void 0;
384
390
  }
385
391
  };
386
- var isMissing = (error) => {
387
- const code = typeof error === "object" && error !== null ? error.code : void 0;
388
- return code === "ENOENT" || code === "ENOTDIR";
389
- };
390
392
  var promptOf2 = (line) => {
391
393
  const prompt = promptOf(line);
392
394
  return prompt?.kind === "typed" || prompt?.kind === "command" ? prompt.text : void 0;
@@ -701,14 +703,6 @@ var parseStatsFile = (text) => text.split("\n").filter((line) => line.trim() !==
701
703
  // src/features.ts
702
704
  import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
703
705
  import { join as join2 } from "node:path";
704
-
705
- // src/runtime.ts
706
- var isMissing2 = (error) => {
707
- const code = typeof error === "object" && error !== null ? error.code : void 0;
708
- return code === "ENOENT" || code === "ENOTDIR";
709
- };
710
-
711
- // src/features.ts
712
706
  var OPTIONS = {
713
707
  output_cleaners: {
714
708
  type: "boolean",
@@ -798,7 +792,7 @@ var loadOptions = (data) => {
798
792
  try {
799
793
  text = readFileSync2(optionsFile(data), "utf8");
800
794
  } catch (error) {
801
- if (isMissing2(error)) {
795
+ if (isMissing(error)) {
802
796
  return readOptions({});
803
797
  }
804
798
  throw error;
@@ -820,6 +814,7 @@ import { join as join6 } from "node:path";
820
814
  import { createHash } from "node:crypto";
821
815
  import { closeSync, fstatSync, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
822
816
  import { dirname as dirname2, join as join3 } from "node:path";
817
+ import * as util from "node:util";
823
818
  var ANSI = /\x1b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])/g;
824
819
  var collapseCr = (line) => {
825
820
  const frames = line.split("\r");
@@ -1083,27 +1078,90 @@ var SECRET_NAME = String.raw`[A-Za-z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|API[_-
1083
1078
  var ASSIGN = String.raw`["']?\s*[:=](?![=>])\s*`;
1084
1079
  var QUOTED_VALUE = String.raw`(["'\x60])([^"'\x60\n]{6,})\2`;
1085
1080
  var BARE_VALUE = String.raw`[\w+/=~-]{6,}(?![\w+/=~(-])`;
1086
- var ISSUER_PREFIX = /^(?:sk-|pk-|rk-|ghp_|gho_|ghu_|ghs_|ghr_|github_pat_|xox[abposr]-|AKIA|ASIA|eyJ|AIza|ya29\.|glpat-|dop_v1_|shpat_|SG\.|npm_)/;
1081
+ var CREDENTIAL_ISSUERS = [
1082
+ { prefix: "npm_", body: "[A-Za-z0-9]{30,}" },
1083
+ { prefix: "ghp_", body: "[A-Za-z0-9]{20,}" },
1084
+ { prefix: "gho_", body: "[A-Za-z0-9]{20,}" },
1085
+ { prefix: "ghu_", body: "[A-Za-z0-9]{20,}" },
1086
+ { prefix: "ghs_", body: "[A-Za-z0-9]{20,}" },
1087
+ { prefix: "ghr_", body: "[A-Za-z0-9]{20,}" },
1088
+ { prefix: "github_pat_", body: "[A-Za-z0-9_]{50,}" },
1089
+ { prefix: "glpat-", body: "[A-Za-z0-9_-]{16,}" },
1090
+ { prefix: "xoxa-", body: "[A-Za-z0-9-]{10,}" },
1091
+ { prefix: "xoxb-", body: "[A-Za-z0-9-]{10,}" },
1092
+ { prefix: "xoxo-", body: "[A-Za-z0-9-]{10,}" },
1093
+ { prefix: "xoxp-", body: "[A-Za-z0-9-]{10,}" },
1094
+ { prefix: "xoxr-", body: "[A-Za-z0-9-]{10,}" },
1095
+ { prefix: "xoxs-", body: "[A-Za-z0-9-]{10,}" },
1096
+ { prefix: "sk-", body: "[A-Za-z0-9_-]{20,}" },
1097
+ { prefix: "pk-", body: "[A-Za-z0-9_-]{20,}" },
1098
+ { prefix: "rk-", body: "[A-Za-z0-9_-]{20,}" },
1099
+ { prefix: "sk_live_", body: "[A-Za-z0-9]{16,}" },
1100
+ { prefix: "rk_live_", body: "[A-Za-z0-9]{16,}" },
1101
+ { prefix: "AKIA", body: String.raw`[0-9A-Z]{16}\b` },
1102
+ { prefix: "ASIA", body: String.raw`[0-9A-Z]{16}\b` },
1103
+ { prefix: "AIza", body: String.raw`[0-9A-Za-z_-]{35}\b` },
1104
+ { prefix: "ya29.", body: "[0-9A-Za-z_-]{20,}" },
1105
+ { prefix: "hf_", body: "[A-Za-z0-9]{30,}" },
1106
+ { prefix: "dop_v1_", body: "[a-f0-9]{60,}" },
1107
+ { prefix: "shpat_", body: "[a-fA-F0-9]{32,}" },
1108
+ // SendGrid: a key id and its secret, dot-separated.
1109
+ { prefix: "SG.", body: String.raw`[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}` },
1110
+ // A JWT: three base64url segments, the first two of which decode from `{"` and so always begin `eyJ` and `ey`.
1111
+ { prefix: "eyJ", body: String.raw`[A-Za-z0-9_-]{9,}\.ey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]*` }
1112
+ ];
1113
+ var CREDENTIAL_ISSUER_PREFIXES = CREDENTIAL_ISSUERS.map(({ prefix }) => prefix);
1114
+ var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
1115
+ var ISSUER_PREFIX = new RegExp(`^(?:${CREDENTIAL_ISSUER_PREFIXES.map(escapeRegExp).join("|")})`);
1116
+ var ISSUED_TOKENS = CREDENTIAL_ISSUERS.map(({ prefix, body }) => new RegExp(String.raw`(?<![\w-])${escapeRegExp(prefix)}${body}`, "g"));
1087
1117
  var NUMERIC_VALUE = /^[\d_,.]+$/;
1088
1118
  var STRUCTURAL_VALUE = /[\s/\\${}]/;
1089
1119
  var ENV_NAME_VALUE = /^[A-Z0-9_]+$/;
1090
1120
  var ENTROPIC_VALUE = /^(?=[^\n]*\d)(?=[^\n]*[A-Za-z])/;
1091
1121
  var HANDWRITTEN_MAX = 12;
1092
1122
  var looksLikeCredential = (value) => ISSUER_PREFIX.test(value) || value.length > HANDWRITTEN_MAX && ENTROPIC_VALUE.test(value) && !NUMERIC_VALUE.test(value) && !STRUCTURAL_VALUE.test(value) && !ENV_NAME_VALUE.test(value);
1123
+ var secretReference = (name) => `{{secret:${name}}}`;
1093
1124
  var SECRET_PATTERNS = [
1094
1125
  [
1095
1126
  new RegExp(`\\b(${SECRET_NAME}${ASSIGN})(?:${QUOTED_VALUE}|(${BARE_VALUE}))`, "gi"),
1096
1127
  // Quoted and bare arrive in different groups because only the quoted one has a quote to put back.
1097
1128
  (match, assignment, quote, quoted, bare) => looksLikeCredential(quoted ?? bare) ? `${assignment}${quote ?? ""}***${quote ?? ""}` : match
1098
1129
  ],
1099
- [/\bAKIA[0-9A-Z]{16}\b/g, "***"],
1130
+ ...ISSUED_TOKENS.map((token) => [token, "***"]),
1100
1131
  // Same rule for a bearer value: prose about tokens carries no digit, every real bearer token does.
1101
1132
  [/\b(Bearer\s+)(?=[\w.-]*\d)[\w.-]{8,}/gi, "$1***"],
1102
- [/\b(https?:\/\/[^:@\s/]+:)[^@\s]+@/gi, "$1***@"]
1133
+ // scheme://user:password@host for any scheme (a postgres DSN as much as an https remote). Any length, unlike the
1134
+ // contract's three-character floor: output reaching a model is not a file someone meant to publish. A `/` ends the
1135
+ // password, so `http://host:3000/@scope/pkg` is a port and a path, and a reference already standing in is kept.
1136
+ [/\b([a-z][a-z0-9+.-]*:\/\/[^:@\s/]+:)(?!\{\{secret:)[^@\s/]+@/gi, "$1***@"]
1103
1137
  ];
1138
+ var MASKED_NEXT = String.raw`(?!(?:\s|\\[nr])*\*\*\*)`;
1139
+ var ARMOR_LINES = String.raw`(?:(?:\s|\\[nr])+(?:[A-Za-z-]+:[^\n\\]*|[A-Za-z0-9+/=]+(?=[ \t]*(?:[\r\n"'\x60<>,;})\]]|\\[nr]|$))))*`;
1140
+ var PRIVATE_KEY_BLOCK = new RegExp(
1141
+ String.raw`(-----BEGIN ((?:[A-Z0-9]+ )*)PRIVATE KEY( BLOCK)?-----)${MASKED_NEXT}(?:[\s\S]*?(-----END \2PRIVATE KEY\3-----)|${ARMOR_LINES})`,
1142
+ "g"
1143
+ );
1144
+ var PUTTY_KEY = new RegExp(String.raw`(PuTTY-User-Key-File-\d:)${MASKED_NEXT}[^\n\\]*${ARMOR_LINES}`, "g");
1145
+ var maskKeyBlocks = (text) => (
1146
+ // The cheap test first: almost no output holds a key, and the block patterns scan the whole body.
1147
+ text.includes("PRIVATE KEY") || text.includes("PuTTY-User-Key-File-") ? text.replace(
1148
+ PRIVATE_KEY_BLOCK,
1149
+ (_block, header, _label, _kind, footer) => footer === void 0 ? `${header}
1150
+ ***` : `${header}
1151
+ ***
1152
+ ${footer}`
1153
+ ).replace(PUTTY_KEY, "$1 ***") : text
1154
+ );
1104
1155
  var SECRET_VALUE_MIN = 12;
1156
+ var SECRET_STORES = [
1157
+ { root: "desiredState", path: ".env", layout: "env", source: "env" },
1158
+ // Pasted on a need's card or the Secrets view (src/secrets/sandbox-secrets.ts).
1159
+ { root: "auth", path: "sandbox-secrets.json", layout: "flat", source: "sandbox" },
1160
+ { root: "desiredState", path: ".secrets.json", layout: "flat", source: "generated" },
1161
+ { root: "auth", path: "capability-secrets.json", layout: "vault", source: "capability" }
1162
+ ];
1105
1163
  var secretValueCache = /* @__PURE__ */ new Map();
1106
- var readIfChanged = (path, name) => {
1164
+ var readIfChanged = (path, layout) => {
1107
1165
  try {
1108
1166
  const descriptor = openSync(path, "r");
1109
1167
  try {
@@ -1113,7 +1171,7 @@ var readIfChanged = (path, name) => {
1113
1171
  if (cached?.stamp === stamp) {
1114
1172
  return cached.values;
1115
1173
  }
1116
- const values = harvest(readFileSync3(descriptor, "utf8"), path, name);
1174
+ const values = harvest(readFileSync3(descriptor, "utf8"), layout);
1117
1175
  secretValueCache.set(path, { stamp, values });
1118
1176
  return values;
1119
1177
  } finally {
@@ -1138,33 +1196,19 @@ var surfaceForms = (value) => {
1138
1196
  }
1139
1197
  return forms;
1140
1198
  };
1141
- var harvest = (text, path, name) => {
1142
- const named = [];
1143
- if (path.endsWith("capability-secrets.json")) {
1144
- for (const [id, entry] of Object.entries(JSON.parse(text))) {
1145
- if (entry === null || typeof entry !== "object") {
1146
- continue;
1147
- }
1148
- for (const [field, value] of Object.entries(entry)) {
1149
- if (typeof value === "string") {
1150
- named.push([name(id, field), value]);
1151
- }
1152
- }
1153
- }
1154
- } else if (path.endsWith(".json")) {
1155
- for (const [key, value] of Object.entries(JSON.parse(text))) {
1156
- if (typeof value === "string") {
1157
- named.push([name(key), value]);
1158
- }
1159
- }
1160
- } else {
1161
- for (const line of text.split("\n")) {
1162
- const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
1163
- if (match !== null) {
1164
- named.push([name(match[1]), (match[2] ?? "").trim().replace(/^(["'])([\s\S]*)\1$/, "$2")]);
1165
- }
1166
- }
1199
+ var namedValues = (text, layout) => {
1200
+ if (layout === "env") {
1201
+ return Object.entries(util.parseEnv(text));
1202
+ }
1203
+ const parsed = JSON.parse(text);
1204
+ if (layout === "flat") {
1205
+ return Object.entries(parsed).filter(([, value]) => typeof value === "string");
1167
1206
  }
1207
+ return Object.entries(parsed).flatMap(
1208
+ ([id, entry]) => entry === null || typeof entry !== "object" ? [] : Object.entries(entry).flatMap(([field, value]) => typeof value === "string" ? [[`${id}/${field}`, value]] : [])
1209
+ );
1210
+ };
1211
+ var harvest = (text, layout) => {
1168
1212
  const byTarget = /* @__PURE__ */ new Map();
1169
1213
  const add = (target, replacement) => {
1170
1214
  const trimmed = target.trim();
@@ -1172,9 +1216,9 @@ var harvest = (text, path, name) => {
1172
1216
  byTarget.set(trimmed, { target: trimmed, replacement });
1173
1217
  }
1174
1218
  };
1175
- for (const [reference, value] of named) {
1219
+ for (const [name, value] of namedValues(text, layout)) {
1176
1220
  for (const form of surfaceForms(value.trim())) {
1177
- add(form, `{{secret:${reference}}}`);
1221
+ add(form, secretReference(name));
1178
1222
  }
1179
1223
  if (value.includes("\n")) {
1180
1224
  for (const line of value.split("\n")) {
@@ -1185,16 +1229,21 @@ var harvest = (text, path, name) => {
1185
1229
  return [...byTarget.values()].toSorted((a, b) => b.target.length - a.target.length);
1186
1230
  };
1187
1231
  var secretValues = (env = process.env) => {
1188
- const authRoot = env.AGENT_AUTH_DIR !== void 0 && env.AGENT_AUTH_DIR !== "" ? env.AGENT_AUTH_DIR : join3(env.WORKSPACE_ROOT ?? "/work", ".intentic/secrets/auth");
1189
- const repo = join3(env.WORKSPACE_ROOT ?? "/work", "desired-state");
1190
- return [
1191
- ...readIfChanged(join3(authRoot, "capability-secrets.json"), (id, field) => `${id}/${field}`),
1192
- ...readIfChanged(join3(repo, ".env"), (key) => key),
1193
- ...readIfChanged(join3(repo, ".secrets.json"), (key) => key)
1194
- ].toSorted((a, b) => b.target.length - a.target.length);
1232
+ const roots = {
1233
+ auth: env.AGENT_AUTH_DIR !== void 0 && env.AGENT_AUTH_DIR !== "" ? env.AGENT_AUTH_DIR : join3(env.WORKSPACE_ROOT ?? "/work", ".intentic/secrets/auth"),
1234
+ desiredState: join3(env.WORKSPACE_ROOT ?? "/work", "desired-state")
1235
+ };
1236
+ return SECRET_STORES.flatMap(({ root, path, layout }) => readIfChanged(join3(roots[root], path), layout)).toSorted(
1237
+ (a, b) => b.target.length - a.target.length
1238
+ );
1239
+ };
1240
+ var redactLines = (lines, values = []) => {
1241
+ const joined = lines.join("\n");
1242
+ const keyed = maskKeyBlocks(joined);
1243
+ return (keyed === joined ? lines : keyed.split("\n")).map((line) => redactLine(line, values));
1195
1244
  };
1196
- var redactText = (text, values = []) => text.split("\n").map((line) => redactLine(line, values)).join("\n");
1197
- var redactLine = (line, values = []) => {
1245
+ var redactText = (text, values = []) => redactLines(text.split("\n"), values).join("\n");
1246
+ var redactLine = (line, values) => {
1198
1247
  let masked = line;
1199
1248
  for (const { target, replacement } of values) {
1200
1249
  if (masked.includes(target)) {
@@ -1305,7 +1354,7 @@ var cleanLines = (lines, { command, exitCode, enabled, values = [] }) => {
1305
1354
  if (enabled.has("redact")) {
1306
1355
  out = ran(
1307
1356
  "redact",
1308
- out.map((line) => redactLine(line, values))
1357
+ redactLines(out, values)
1309
1358
  );
1310
1359
  }
1311
1360
  return { lines: out, stages };
@@ -1593,7 +1642,7 @@ var runHook = async (handle) => {
1593
1642
  `);
1594
1643
  }
1595
1644
  } catch (error) {
1596
- process.stderr.write(`intentic: ${error instanceof Error ? error.message : String(error)}
1645
+ process.stderr.write(`intentic: ${errorMessage(error)}
1597
1646
  `);
1598
1647
  }
1599
1648
  };
@@ -1653,7 +1702,7 @@ var readSessions = (data) => {
1653
1702
  try {
1654
1703
  text = readFileSync4(sessionsFile(data), "utf8");
1655
1704
  } catch (error) {
1656
- if (isMissing2(error)) {
1705
+ if (isMissing(error)) {
1657
1706
  return [];
1658
1707
  }
1659
1708
  throw error;
@@ -1691,7 +1740,7 @@ var readText = (path) => {
1691
1740
  try {
1692
1741
  return readFileSync5(path, "utf8");
1693
1742
  } catch (error) {
1694
- if (isMissing2(error)) {
1743
+ if (isMissing(error)) {
1695
1744
  return void 0;
1696
1745
  }
1697
1746
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentic/claude-plugin",
3
- "version": "1.331.0",
3
+ "version": "1.333.0",
4
4
  "description": "The intentic Claude Code plugin: trimmed Bash output, a project map and field notes at session start, iq search and fileq teaching, and a /intentic:stats report of what each saved",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,8 +40,9 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@intentic/agent-context": "0.0.0",
43
- "@intentic/fileq": "1.331.0",
44
- "@intentic/iq": "1.331.0",
43
+ "@intentic/base": "1.333.0",
44
+ "@intentic/fileq": "1.333.0",
45
+ "@intentic/iq": "1.333.0",
45
46
  "@intentic/output-cleaners": "0.0.0",
46
47
  "@intentic/testing": "0.0.0",
47
48
  "@intentic/tsconfig": "0.0.0",
@@ -52,7 +53,7 @@
52
53
  },
53
54
  "scripts": {
54
55
  "build": "tsgo --noEmit && node build.mjs",
55
- "typecheck": "tsgo --noEmit -p tsconfig.test.json",
56
+ "typecheck": "bun check -p tsconfig.test.json",
56
57
  "test": "suites",
57
58
  "test:watch": "suites --watch",
58
59
  "test:mod": "node test-mod.mjs",