@harnessforce/claude-code-plugin 0.1.0 → 0.3.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.
@@ -1,18 +1,70 @@
1
1
  let node_os = require("node:os");
2
- let node_perf_hooks = require("node:perf_hooks");
3
2
  let node_path = require("node:path");
3
+ let node_perf_hooks = require("node:perf_hooks");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  let node_child_process = require("node:child_process");
7
7
  let node_fs = require("node:fs");
8
- //#region ../../packages/agent-core/src/hook-warning.ts
9
- const RESTART_FAILED_WARNING = "Harnessforceのhookを起動し直せませんでした";
10
- const INVALID_ENDPOINT_WARNING = "Harnessforceの送信先が不正です。`harnessforce init`を実行し直してください";
11
- const REGISTRATION_FAILED_WARNING = "Harnessforceへのsession登録に失敗しました。通信を確かめてください";
12
- const notConfiguredWarning = (agent) => agent === "claude_code" ? "Harnessforceの送信が設定されていません。`/harnessforce:setup`を実行してください" : "Harnessforceの送信が設定されていません。`harnessforce init`を実行してください";
13
- const warningOnlyOutput = (message) => `${JSON.stringify({ systemMessage: message })}\n`;
14
- //#endregion
15
- //#region ../../packages/agent-core/src/config/canonical.ts
8
+ const NO_WHITE_SPACE = new RegExp(`^[^\\t-\\r \\u0085\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000]+$`);
9
+ const isToken = (value, maxLength = 256) => {
10
+ const length = [...value].length;
11
+ return length >= 1 && length <= maxLength && NO_WHITE_SPACE.test(value);
12
+ };
13
+ const INSTANT_PATTERN = `^(?:1(?:6(?:7[7-9]|[89]\\d)|[7-9]\\d\\d)|2(?:[01]\\d\\d|2(?:[0-5]\\d|6[0-2])))-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$`;
14
+ const INSTANT_PARTS = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:Z|([+-])(\d{2}):(\d{2}))$/;
15
+ const INSTANT = new RegExp(INSTANT_PATTERN);
16
+ const MIN_INSTANT_MS = Date.UTC(1677, 8, 21, 0, 12, 44);
17
+ const MAX_INSTANT_MS = Date.UTC(2262, 3, 11, 23, 47, 16);
18
+ function isInstant(value) {
19
+ const parts = INSTANT.test(value) ? INSTANT_PARTS.exec(value) : null;
20
+ if (!parts) return false;
21
+ const [, ...fields] = parts;
22
+ const [year, month, day, hour, minute, second] = fields.slice(0, 6).map(Number);
23
+ const [fraction = "", sign, offsetHours = "0", offsetMinutes = "0"] = fields.slice(6);
24
+ const localMs = Date.UTC(year, month - 1, day, hour, minute, second);
25
+ const local = new Date(localMs);
26
+ if (!(local.getUTCFullYear() === year && local.getUTCMonth() === month - 1 && local.getUTCDate() === day && local.getUTCHours() === hour && local.getUTCMinutes() === minute && local.getUTCSeconds() === second)) return false;
27
+ const secondMs = localMs - (sign === "-" ? -1 : 1) * (Number(offsetHours) * 60 + Number(offsetMinutes)) * 6e4;
28
+ const nanos = Number(fraction.slice(0, 9) || "0");
29
+ return secondMs >= MIN_INSTANT_MS && !(secondMs > MAX_INSTANT_MS || secondMs === MAX_INSTANT_MS && nanos > 0);
30
+ }
31
+ // ../../packages/semconv/dist/repository.js
32
+ const SCHEME_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
33
+ const SCP_LIKE = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(.+)$/;
34
+ const HOST = /^[a-z0-9.-]+$/;
35
+ const SEGMENT = /^[A-Za-z0-9_.-]+$/;
36
+ function splitRemote(remote) {
37
+ if (SCHEME_URL.test(remote)) {
38
+ let url;
39
+ try {
40
+ url = new URL(remote);
41
+ } catch {
42
+ return;
43
+ }
44
+ return url.protocol === "file:" ? void 0 : {
45
+ host: url.hostname,
46
+ path: url.pathname
47
+ };
48
+ }
49
+ const scp = SCP_LIKE.exec(remote);
50
+ return scp?.[1] && scp[2] ? {
51
+ host: scp[1],
52
+ path: scp[2]
53
+ } : void 0;
54
+ }
55
+ function normalizeRepository(remoteUrl) {
56
+ const location = splitRemote(remoteUrl.trim());
57
+ if (!location) return void 0;
58
+ const lowerHost = location.host.toLowerCase();
59
+ const host = lowerHost === "ssh.github.com" ? "github.com" : lowerHost;
60
+ const segments = location.path.replace(/\/+$/, "").replace(/\.git$/, "").replace(/^\/+/, "").split("/");
61
+ if (segments.length !== 2 || !HOST.test(host) || !segments.every((segment) => SEGMENT.test(segment))) return void 0;
62
+ const [owner, name] = host === "github.com" ? segments.map((segment) => segment.toLowerCase()) : segments;
63
+ return `${host}/${owner}/${name}`;
64
+ }
65
+ // ../../packages/agent-core/src/object.ts
66
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
67
+ // ../../packages/agent-core/src/config/canonical.ts
16
68
  const byCodeUnit$1 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
17
69
  function canonicalJson(value) {
18
70
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
@@ -28,11 +80,7 @@ function hashFileContent(content) {
28
80
  function sortComponents(components) {
29
81
  return [...components].sort((a, b) => byCodeUnit$1(a.kind, b.kind) || byCodeUnit$1(a.source, b.source) || byCodeUnit$1(a.id, b.id));
30
82
  }
31
- //#endregion
32
- //#region ../../packages/agent-core/src/object.ts
33
- const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
34
- //#endregion
35
- //#region ../../packages/agent-core/src/config/files.ts
83
+ // ../../packages/agent-core/src/config/files.ts
36
84
  const MAX_DEPTH = 8;
37
85
  const MAX_FILE_BYTES = 1048576;
38
86
  var CollectionExpired = class extends Error {};
@@ -93,28 +141,171 @@ async function listDirectories(dir, guard) {
93
141
  for (const entry of await entries(dir, guard)) if (await kindOf(dir, entry, guard) === "directory") names.push(entry.name);
94
142
  return names;
95
143
  }
96
- //#endregion
97
- //#region ../../packages/agent-core/src/managed.ts
144
+ // ../../packages/agent-core/src/config/hash-input.ts
145
+ function withoutSecrets(entry) {
146
+ const { env, headers, url, args: _args, ...rest } = entry;
147
+ const result = rest;
148
+ if (isObject(env)) result.env = Object.keys(env).sort(byCodeUnit$1);
149
+ if (isObject(headers)) result.headers = Object.keys(headers).sort(byCodeUnit$1);
150
+ const strippedUrl = typeof url === "string" ? withoutCredentials(url) : void 0;
151
+ if (strippedUrl !== void 0) result.url = strippedUrl;
152
+ return result;
153
+ }
154
+ function withoutCredentials(url) {
155
+ let parsed;
156
+ try {
157
+ parsed = new URL(url);
158
+ } catch {
159
+ return;
160
+ }
161
+ parsed.username = "";
162
+ parsed.password = "";
163
+ parsed.search = "";
164
+ parsed.hash = "";
165
+ return parsed.href;
166
+ }
167
+ const reduceHookGroup = (group) => isObject(group) && Array.isArray(group.hooks) ? {
168
+ ...group,
169
+ hooks: group.hooks.map((handler) => isObject(handler) ? withoutSecrets(handler) : handler)
170
+ } : group;
171
+ function hashInput(kind, value) {
172
+ if (kind === "mcp_server" && isObject(value)) return withoutSecrets(value);
173
+ if (kind === "hook" && Array.isArray(value)) return value.map(reduceHookGroup);
174
+ return value;
175
+ }
176
+ // ../../packages/agent-core/src/config/scope.ts
177
+ const MAX_COMPONENTS = 1e3;
178
+ var TooManyComponents = class extends Error {};
179
+ var Sink = class {
180
+ guard;
181
+ components = [];
182
+ constructor(guard) {
183
+ this.guard = guard;
184
+ }
185
+ add(component) {
186
+ if (!isToken(component.id)) return;
187
+ if (this.components.length >= MAX_COMPONENTS) throw new TooManyComponents();
188
+ this.components.push(component);
189
+ }
190
+ hasDuplicates() {
191
+ return new Set(this.components.map((c) => `${c.kind}\0${c.source}\0${c.id}`)).size !== this.components.length;
192
+ }
193
+ scope = (source, idPrefix = "", version) => {
194
+ const add = (kind, id, hash) => this.add({
195
+ kind,
196
+ source,
197
+ id: `${idPrefix}${id}`,
198
+ ...version === void 0 ? {} : { version },
199
+ hash
200
+ });
201
+ return {
202
+ guard: this.guard,
203
+ addValue: (kind, id, value) => add(kind, id, hashValue(hashInput(kind, value))),
204
+ addFile: async (kind, id, path) => {
205
+ const content = await readFileIfExists(path, this.guard);
206
+ if (content) add(kind, id, hashFileContent(content));
207
+ }
208
+ };
209
+ };
210
+ };
211
+ const absoluteEnv = (value) => value && (0, node_path.isAbsolute)(value) ? value : void 0;
212
+ const withoutExt = (path, ext) => path.slice(0, -ext.length);
213
+ const colonName = (path) => withoutExt(path, ".md").replace(/\//g, ":");
214
+ async function collectLayout(s, layout) {
215
+ if (layout.skills) for (const name of await listDirectories(layout.skills, s.guard)) await s.addFile("skill", name, (0, node_path.join)(layout.skills, name, "SKILL.md"));
216
+ if (layout.agents) for (const path of await listFiles(layout.agents, ".md", true, s.guard)) await s.addFile("agent", colonName(path), (0, node_path.join)(layout.agents, path));
217
+ if (layout.commands) for (const path of await listFiles(layout.commands, ".md", true, s.guard)) await s.addFile("command", colonName(path), (0, node_path.join)(layout.commands, path));
218
+ if (layout.workflows) for (const path of await listFiles(layout.workflows, ".js", false, s.guard)) await s.addFile("workflow", withoutExt(path, ".js"), (0, node_path.join)(layout.workflows, path));
219
+ }
220
+ async function collectRules(s, base, names, rulesDir) {
221
+ for (const name of names) await s.addFile("rule", name, (0, node_path.join)(base, name));
222
+ if (rulesDir) for (const path of await listFiles(rulesDir, ".md", true, s.guard)) await s.addFile("rule", `rules/${path}`, (0, node_path.join)(rulesDir, path));
223
+ }
224
+ function collectHooks(s, hooks) {
225
+ if (!isObject(hooks)) return;
226
+ for (const [event, value] of Object.entries(hooks)) s.addValue("hook", event, value);
227
+ }
228
+ function collectMcpServers(s, servers) {
229
+ if (!isObject(servers)) return;
230
+ for (const [name, value] of Object.entries(servers)) if (isObject(value)) s.addValue("mcp_server", name, value);
231
+ }
232
+ // ../../packages/agent-core/src/hook-warning.ts
233
+ const RESTART_FAILED_WARNING = "Could not restart the Harnessforce hook";
234
+ const INVALID_ENDPOINT_WARNING = "The Harnessforce destination is invalid. Run `harnessforce init` again";
235
+ const REGISTRATION_FAILED_WARNING = "Harnessforce session registration failed. Check your network connection";
236
+ const notConfiguredWarning = (agent) => agent === "claude_code" ? "Harnessforce sending is not configured. Run `/harnessforce:setup`" : "Harnessforce sending is not configured. Run `harnessforce init`";
237
+ const stopped = (agent) => agent === "claude_code" ? "telemetry is not being sent" : "sessions are not being registered";
238
+ const keyMissingWarning = (agent) => `Harnessforce cannot find your ingest key, so ${stopped(agent)}. Run \`harnessforce init\``;
239
+ const keyUnreadableWarning = (agent) => `Harnessforce could not read your ingest key from the OS keychain, so ${agent === "claude_code" ? "telemetry may not be sent" : "sessions may not be registered"}. Unlock the keychain or run \`harnessforce init\``;
240
+ const DESTINATION_MISMATCH_WARNING = {
241
+ claude_code: "Telemetry is not being sent because the Harnessforce destination differs from the one saved by `harnessforce init`. Check this repository's settings or run `harnessforce init` again",
242
+ codex: "Sessions are not being registered because the Harnessforce destination differs from the one saved by `harnessforce init`. Run `harnessforce init` again"
243
+ };
244
+ const TELEMETRY_NOT_CONFIGURED_WARNING = "Claude Code telemetry for Harnessforce is not configured. Run `harnessforce init`";
245
+ const warningOnlyOutput = (message) => `${JSON.stringify({ systemMessage: message })}\n`;
246
+ // ../../packages/agent-core/src/managed.ts
98
247
  const MANAGED_DIRS = {
99
248
  darwin: "/Library/Application Support/ClaudeCode",
100
249
  win32: "C:\\Program Files\\ClaudeCode"
101
250
  };
102
251
  const managedDirFor = (platform) => MANAGED_DIRS[platform] ?? "/etc/claude-code";
103
- const noGuard = () => {};
252
+ const noGuard$2 = () => {};
104
253
  async function readManagedEnv(managedDir, names) {
105
254
  const dropInDir = (0, node_path.join)(managedDir, "managed-settings.d");
106
- const dropIns = (await listFiles(dropInDir, ".json", false, noGuard)).sort(byCodeUnit$1);
255
+ const dropIns = (await listFiles(dropInDir, ".json", false, noGuard$2)).sort(byCodeUnit$1);
107
256
  const files = [(0, node_path.join)(managedDir, "managed-settings.json"), ...dropIns.map((name) => (0, node_path.join)(dropInDir, name))];
108
257
  const values = Object.fromEntries(names.map((name) => [name, void 0]));
109
258
  for (const file of files) {
110
- const env = (await readJsonObjectFile(file, noGuard))?.env;
259
+ const env = (await readJsonObjectFile(file, noGuard$2))?.env;
111
260
  if (!isObject(env)) continue;
112
261
  for (const name of names) if (typeof env[name] === "string") values[name] = env[name];
113
262
  }
114
263
  return values;
115
264
  }
116
- //#endregion
117
- //#region ../../packages/agent-core/src/process/lookup.ts
265
+ // ../../packages/agent-core/src/plugin-auto-update.ts
266
+ const MARKETPLACE_NAME = "harnessforce-agent";
267
+ const MARKETPLACE_KEYS = ["extraKnownMarketplaces", "additionalMarketplaces"];
268
+ const DISABLING_VARIABLES = [
269
+ "DISABLE_UPDATES",
270
+ "DISABLE_AUTOUPDATER",
271
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"
272
+ ];
273
+ const noGuard$1 = () => {};
274
+ function marketplacesOf(settings) {
275
+ const key = MARKETPLACE_KEYS.find((name) => settings?.[name] !== void 0);
276
+ const value = key === void 0 ? void 0 : settings?.[key];
277
+ return isObject(value) ? value : void 0;
278
+ }
279
+ const declaresEntry = (settings) => MARKETPLACE_KEYS.some((key) => {
280
+ const value = settings?.[key];
281
+ return isObject(value) && value["harnessforce-agent"] !== void 0;
282
+ });
283
+ async function managedFiles(managedDir) {
284
+ const dropInDir = (0, node_path.join)(managedDir, "managed-settings.d");
285
+ const dropIns = (await listFiles(dropInDir, ".json", false, noGuard$1)).sort(byCodeUnit$1);
286
+ return [(0, node_path.join)(managedDir, "managed-settings.json"), ...dropIns.map((name) => (0, node_path.join)(dropInDir, name))];
287
+ }
288
+ /**
289
+ * Claude Codeがpluginを自動更新するとみなせるか(client-updates.md「表示」)。確かめられない場合は偽とし、outdatedを表示する。
290
+ * user settingsのentryが真で、優先度の高いfileが同じ名前のentryを持たず、自動更新を止める変数が無い場合だけ真とする。
291
+ */
292
+ async function isPluginAutoUpdateEffective(options) {
293
+ const { env } = options;
294
+ if (DISABLING_VARIABLES.some((name) => env[name] === "1") && env.FORCE_AUTOUPDATE_PLUGINS !== "1") return false;
295
+ const higher = [
296
+ ...await managedFiles(options.managedDir),
297
+ (0, node_path.join)(options.projectDir, ".claude", "settings.json"),
298
+ (0, node_path.join)(options.projectDir, ".claude", "settings.local.json")
299
+ ];
300
+ for (const file of higher) {
301
+ if (!await (0, node_fs_promises.access)(file).then(() => true, () => false)) continue;
302
+ const settings = await readJsonObjectFile(file, noGuard$1);
303
+ if (!settings || declaresEntry(settings)) return false;
304
+ }
305
+ const entry = marketplacesOf(await readJsonObjectFile(options.userSettingsPath, noGuard$1))?.[MARKETPLACE_NAME];
306
+ return isObject(entry) && entry.autoUpdate === true;
307
+ }
308
+ // ../../packages/agent-core/src/process/lookup.ts
118
309
  const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
119
310
  const NPM_SHIM_MAX_BYTES = 65536;
120
311
  const SHEBANG_MAX_BYTES = 256;
@@ -295,8 +486,7 @@ async function commandLineFor(file, args, context, { quoteArgs = true } = {}) {
295
486
  verbatim: true
296
487
  };
297
488
  }
298
- //#endregion
299
- //#region ../../packages/agent-core/src/process/git.ts
489
+ // ../../packages/agent-core/src/process/git.ts
300
490
  const GIT_TIMEOUT_MS = 1e3;
301
491
  const execGit = (file, args, env, timeoutMs) => new Promise((resolve) => {
302
492
  (0, node_child_process.execFile)(file, args, {
@@ -334,8 +524,7 @@ function createGitRunner({ platform, env, processCwd, excludeTarget, timeoutMs =
334
524
  ], gitEnv, timeoutMs);
335
525
  };
336
526
  }
337
- //#endregion
338
- //#region ../../packages/agent-core/src/process/runtime-env.ts
527
+ // ../../packages/agent-core/src/process/runtime-env.ts
339
528
  const RUNTIME_VARIABLES = [
340
529
  "NODE_TLS_REJECT_UNAUTHORIZED",
341
530
  "NODE_EXTRA_CA_CERTS",
@@ -399,123 +588,207 @@ function relaunchWithoutRuntimeVariables({ platform, env, stash, spawnSelf: star
399
588
  }));
400
589
  });
401
590
  }
402
- const NO_WHITE_SPACE = new RegExp(`^[^\\t-\\r \\u0085\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000]+$`);
403
- const isToken = (value, maxLength = 256) => {
404
- const length = [...value].length;
405
- return length >= 1 && length <= maxLength && NO_WHITE_SPACE.test(value);
591
+ // ../../packages/agent-core/src/telemetry-settings.ts
592
+ const EXPECTED = {
593
+ CLAUDE_CODE_ENABLE_TELEMETRY: (value) => value === "1",
594
+ OTEL_LOGS_EXPORTER: (value) => includesOtlp(value),
595
+ OTEL_METRICS_EXPORTER: (value) => includesOtlp(value),
596
+ OTEL_TRACES_EXPORTER: (value) => includesOtlp(value),
597
+ OTEL_EXPORTER_OTLP_PROTOCOL: (value) => value === "http/protobuf"
406
598
  };
407
- const INSTANT_PATTERN = `^(?:1(?:6(?:7[7-9]|[89]\\d)|[7-9]\\d\\d)|2(?:[01]\\d\\d|2(?:[0-5]\\d|6[0-2])))-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$`;
408
- const INSTANT_PARTS = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:Z|([+-])(\d{2}):(\d{2}))$/;
409
- const INSTANT = new RegExp(INSTANT_PATTERN);
410
- const MIN_INSTANT_MS = Date.UTC(1677, 8, 21, 0, 12, 44);
411
- const MAX_INSTANT_MS = Date.UTC(2262, 3, 11, 23, 47, 16);
412
- function isInstant(value) {
413
- const parts = INSTANT.test(value) ? INSTANT_PARTS.exec(value) : null;
414
- if (!parts) return false;
415
- const [, ...fields] = parts;
416
- const [year, month, day, hour, minute, second] = fields.slice(0, 6).map(Number);
417
- const [fraction = "", sign, offsetHours = "0", offsetMinutes = "0"] = fields.slice(6);
418
- const localMs = Date.UTC(year, month - 1, day, hour, minute, second);
419
- const local = new Date(localMs);
420
- if (!(local.getUTCFullYear() === year && local.getUTCMonth() === month - 1 && local.getUTCDate() === day && local.getUTCHours() === hour && local.getUTCMinutes() === minute && local.getUTCSeconds() === second)) return false;
421
- const secondMs = localMs - (sign === "-" ? -1 : 1) * (Number(offsetHours) * 60 + Number(offsetMinutes)) * 6e4;
422
- const nanos = Number(fraction.slice(0, 9) || "0");
423
- return secondMs >= MIN_INSTANT_MS && !(secondMs > MAX_INSTANT_MS || secondMs === MAX_INSTANT_MS && nanos > 0);
424
- }
425
- //#endregion
426
- //#region ../../packages/semconv/dist/repository.js
427
- const SCHEME_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
428
- const SCP_LIKE = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(.+)$/;
429
- const HOST = /^[a-z0-9.-]+$/;
430
- const SEGMENT = /^[A-Za-z0-9_.-]+$/;
431
- function splitRemote(remote) {
432
- if (SCHEME_URL.test(remote)) {
433
- let url;
434
- try {
435
- url = new URL(remote);
436
- } catch {
437
- return;
438
- }
439
- return url.protocol === "file:" ? void 0 : {
440
- host: url.hostname,
441
- path: url.pathname
442
- };
599
+ const NAMES = Object.keys(EXPECTED);
600
+ const includesOtlp = (value) => value.split(",").some((exporter) => exporter.trim() === "otlp");
601
+ const noGuard = () => {};
602
+ async function isTelemetryConfigured(options) {
603
+ const managed = await readManagedEnv(options.managedDir, NAMES);
604
+ const settings = await readJsonObjectFile(options.userSettingsPath, noGuard).catch(() => void 0);
605
+ const userEnv = isObject(settings?.env) ? settings.env : {};
606
+ return NAMES.every((name) => {
607
+ const value = managed[name] ?? userEnv[name];
608
+ return typeof value === "string" && EXPECTED[name](value);
609
+ }) && typeof settings?.otelHeadersHelper === "string";
610
+ }
611
+ // ../../packages/agent-core/src/url.ts
612
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
613
+ "localhost",
614
+ "127.0.0.1",
615
+ "[::1]"
616
+ ]);
617
+ function parseAllowedUrl(value) {
618
+ if (!value) return void 0;
619
+ let url;
620
+ try {
621
+ url = new URL(value);
622
+ } catch {
623
+ return;
443
624
  }
444
- const scp = SCP_LIKE.exec(remote);
445
- return scp?.[1] && scp[2] ? {
446
- host: scp[1],
447
- path: scp[2]
448
- } : void 0;
625
+ return url.protocol === "https:" || url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname) ? url : void 0;
449
626
  }
450
- function normalizeRepository(remoteUrl) {
451
- const location = splitRemote(remoteUrl.trim());
452
- if (!location) return void 0;
453
- const lowerHost = location.host.toLowerCase();
454
- const host = lowerHost === "ssh.github.com" ? "github.com" : lowerHost;
455
- const segments = location.path.replace(/\/+$/, "").replace(/\.git$/, "").replace(/^\/+/, "").split("/");
456
- if (segments.length !== 2 || !HOST.test(host) || !segments.every((segment) => SEGMENT.test(segment))) return void 0;
457
- const [owner, name] = host === "github.com" ? segments.map((segment) => segment.toLowerCase()) : segments;
458
- return `${host}/${owner}/${name}`;
627
+ function underBase(base, path) {
628
+ const url = new URL(base.origin);
629
+ url.pathname = `${base.pathname.replace(/\/+$/, "")}/${path}`;
630
+ return url;
459
631
  }
460
- //#endregion
461
- //#region ../../packages/agent-core/src/config/scope.ts
462
- const MAX_COMPONENTS = 1e3;
463
- var TooManyComponents = class extends Error {};
464
- var Sink = class {
465
- guard;
466
- components = [];
467
- constructor(guard) {
468
- this.guard = guard;
632
+ // ../../packages/agent-core/src/client-update.ts
633
+ const CLIENT_UPDATE_HEADER = "harnessforce-client-update";
634
+ const UPDATE_COMMANDS = {
635
+ cli: "npm install -g @harnessforce/cli@latest",
636
+ "claude-code-plugin": "claude plugin update harnessforce@harnessforce-agent"
637
+ };
638
+ const DISPLAY_NAMES = {
639
+ cli: "CLI",
640
+ "claude-code-plugin": "plugin"
641
+ };
642
+ const DAY_MS = 864e5;
643
+ const RECHECK_TIMEOUT_MS = 2e3;
644
+ const userAgentOf = (client) => `harnessforce-${client.component}${client.dev ? "-dev" : ""}/${client.version}`;
645
+ const STATUSES = /* @__PURE__ */ new Set([
646
+ "current",
647
+ "outdated",
648
+ "deprecated",
649
+ "unsupported"
650
+ ]);
651
+ const REASONS = /* @__PURE__ */ new Set(["security", "compatibility"]);
652
+ function parseClientUpdateHeader(value) {
653
+ if (!value) return void 0;
654
+ const members = /* @__PURE__ */ new Map();
655
+ for (const member of value.split(",")) {
656
+ const [key, raw] = member.trim().split("=", 2);
657
+ if (key && raw !== void 0) members.set(key, raw);
469
658
  }
470
- add(component) {
471
- if (!isToken(component.id)) return;
472
- if (this.components.length >= MAX_COMPONENTS) throw new TooManyComponents();
473
- this.components.push(component);
659
+ const status = members.get("status");
660
+ if (!status || !STATUSES.has(status)) return void 0;
661
+ const update = { status };
662
+ const latest = /^"([^"\\]*)"$/.exec(members.get("latest") ?? "")?.[1];
663
+ if (latest) update.latest = latest;
664
+ const reason = members.get("reason");
665
+ if (reason && REASONS.has(reason)) update.reason = reason;
666
+ const seconds = /^@(\d+)$/.exec(members.get("deadline") ?? "")?.[1];
667
+ if (seconds) update.deadline = (/* @__PURE__ */ new Date(Number(seconds) * 1e3)).toISOString();
668
+ return isComplete(update) ? update : void 0;
669
+ }
670
+ function isComplete(update) {
671
+ switch (update.status) {
672
+ case "outdated": return Boolean(update.latest);
673
+ case "deprecated": return Boolean(update.reason) && !Number.isNaN(Date.parse(update.deadline ?? ""));
674
+ case "unsupported": return Boolean(update.reason);
675
+ default: return true;
474
676
  }
475
- hasDuplicates() {
476
- return new Set(this.components.map((c) => `${c.kind}\0${c.source}\0${c.id}`)).size !== this.components.length;
677
+ }
678
+ function stateFrom(value, version) {
679
+ if (!isObject(value) || value.version !== version) return void 0;
680
+ if (typeof value.receivedAt !== "string") return void 0;
681
+ if (typeof value.status !== "string" || !STATUSES.has(value.status)) return void 0;
682
+ const state = value;
683
+ return isComplete(state) ? state : void 0;
684
+ }
685
+ async function readUpdateState(path, version) {
686
+ const saved = await readSavedUpdateState(path);
687
+ return saved?.version === version ? saved : void 0;
688
+ }
689
+ async function readSavedUpdateState(path) {
690
+ try {
691
+ const value = JSON.parse(await (0, node_fs_promises.readFile)(path, "utf8"));
692
+ return isObject(value) && typeof value.version === "string" ? stateFrom(value, value.version) : void 0;
693
+ } catch {
694
+ return;
477
695
  }
478
- scope = (source, idPrefix = "", version) => {
479
- const add = (kind, id, hash) => this.add({
480
- kind,
481
- source,
482
- id: `${idPrefix}${id}`,
483
- ...version === void 0 ? {} : { version },
484
- hash
485
- });
486
- return {
487
- guard: this.guard,
488
- addValue: (kind, id, value) => add(kind, id, hashValue(value)),
489
- addFile: async (kind, id, path) => {
490
- const content = await readFileIfExists(path, this.guard);
491
- if (content) add(kind, id, hashFileContent(content));
492
- }
696
+ }
697
+ async function writeUpdateState(path, state) {
698
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(path), {
699
+ recursive: true,
700
+ mode: 448
701
+ });
702
+ const temporary = `${path}.${(0, node_crypto.randomUUID)()}.tmp`;
703
+ await (0, node_fs_promises.writeFile)(temporary, `${JSON.stringify(state)}\n`, { mode: 384 });
704
+ await (0, node_fs_promises.rename)(temporary, path);
705
+ }
706
+ /**
707
+ * Harnessforceへの要求に`User-Agent`を付け、応答の`Harnessforce-Client-Update`を状態のfileへ保存するfetch。
708
+ * 保存に失敗しても要求の結果は変えない。statePathが無い(pluginのdata directoryが無い)場合は版の申告だけを行う。
709
+ */
710
+ function recordingFetch(fetchImpl, client, statePath, now) {
711
+ return async (url, init) => {
712
+ const headers = {
713
+ ...Object.fromEntries(new Headers(init.headers)),
714
+ "user-agent": userAgentOf(client)
493
715
  };
716
+ const response = await fetchImpl(url, {
717
+ ...init,
718
+ headers
719
+ });
720
+ const update = parseClientUpdateHeader(response.headers.get(CLIENT_UPDATE_HEADER));
721
+ if (update && statePath) {
722
+ const previous = await readUpdateState(statePath, client.version);
723
+ await writeUpdateState(statePath, {
724
+ ...update,
725
+ version: client.version,
726
+ receivedAt: now().toISOString(),
727
+ ...previous?.outdatedShownAt ? { outdatedShownAt: previous.outdatedShownAt } : {}
728
+ }).catch(() => {});
729
+ }
730
+ return response;
494
731
  };
495
- };
496
- const absoluteEnv = (value) => value && (0, node_path.isAbsolute)(value) ? value : void 0;
497
- const withoutExt = (path, ext) => path.slice(0, -ext.length);
498
- const colonName = (path) => withoutExt(path, ".md").replace(/\//g, ":");
499
- async function collectLayout(s, layout) {
500
- if (layout.skills) for (const name of await listDirectories(layout.skills, s.guard)) await s.addFile("skill", name, (0, node_path.join)(layout.skills, name, "SKILL.md"));
501
- if (layout.agents) for (const path of await listFiles(layout.agents, ".md", true, s.guard)) await s.addFile("agent", colonName(path), (0, node_path.join)(layout.agents, path));
502
- if (layout.commands) for (const path of await listFiles(layout.commands, ".md", true, s.guard)) await s.addFile("command", colonName(path), (0, node_path.join)(layout.commands, path));
503
- if (layout.workflows) for (const path of await listFiles(layout.workflows, ".js", false, s.guard)) await s.addFile("workflow", withoutExt(path, ".js"), (0, node_path.join)(layout.workflows, path));
504
732
  }
505
- async function collectRules(s, base, names, rulesDir) {
506
- for (const name of names) await s.addFile("rule", name, (0, node_path.join)(base, name));
507
- if (rulesDir) for (const path of await listFiles(rulesDir, ".md", true, s.guard)) await s.addFile("rule", `rules/${path}`, (0, node_path.join)(rulesDir, path));
733
+ const utcDate = (instant) => new Intl.DateTimeFormat("en-CA", {
734
+ timeZone: "UTC",
735
+ year: "numeric",
736
+ month: "2-digit",
737
+ day: "2-digit"
738
+ }).format(new Date(instant));
739
+ const unsupportedLine = (component) => `This Harnessforce ${DISPLAY_NAMES[component]} version is no longer supported, so nothing is being sent to Harnessforce. Update it: \`${UPDATE_COMMANDS[component]}\``;
740
+ function updateLine(component, update) {
741
+ const name = DISPLAY_NAMES[component];
742
+ const command = UPDATE_COMMANDS[component];
743
+ switch (update.status) {
744
+ case "unsupported": return unsupportedLine(component);
745
+ case "deprecated": {
746
+ const stops = `stops sending on ${utcDate(update.deadline ?? "")} (00:00 UTC)`;
747
+ return update.reason === "security" ? `A security update for Harnessforce ${name} is available. This version ${stops}. Update it: \`${command}\`` : `This Harnessforce ${name} version ${stops}. Update it: \`${command}\``;
748
+ }
749
+ case "outdated": return `Harnessforce ${name} ${update.latest} is available. Update it: \`${command}\``;
750
+ default: return;
751
+ }
508
752
  }
509
- function collectHooks(s, hooks) {
510
- if (!isObject(hooks)) return;
511
- for (const [event, value] of Object.entries(hooks)) s.addValue("hook", event, value);
753
+ function shouldShowOutdated(state, now, env) {
754
+ if (state.status !== "outdated" || env.NO_UPDATE_NOTIFIER || env.CI) return false;
755
+ const shownAt = state.outdatedShownAt ? Date.parse(state.outdatedShownAt) : NaN;
756
+ return Number.isNaN(shownAt) || now.getTime() - shownAt >= DAY_MS;
512
757
  }
513
- function collectMcpServers(s, servers) {
514
- if (!isObject(servers)) return;
515
- for (const [name, value] of Object.entries(servers)) if (isObject(value)) s.addValue("mcp_server", name, value);
758
+ const markOutdatedShown = (path, state, now) => writeUpdateState(path, {
759
+ ...state,
760
+ outdatedShownAt: now.toISOString()
761
+ });
762
+ /**
763
+ * 表示する1行。outdatedは24時間に1回までとし、表示する場合はその時刻を書く。isSuppressedは、pluginを
764
+ * Claude Codeが自動更新する間など、呼び出し側がoutdatedを示さない条件。
765
+ */
766
+ async function noticeLine(component, statePath, state, options) {
767
+ if (state.status === "outdated") {
768
+ if (!shouldShowOutdated(state, options.now, options.env) || await options.isSuppressed?.()) return void 0;
769
+ await markOutdatedShown(statePath, state, options.now).catch(() => {});
770
+ }
771
+ return updateLine(component, state);
772
+ }
773
+ /**
774
+ * unsupportedを受けてから24時間以上経っていれば、資格情報を送らずに`GET /v1/client-status`で確かめ直す
775
+ * (client-updates.md「送信の停止」)。失敗した場合は状態を変えない。
776
+ */
777
+ async function recheckClientStatus(options) {
778
+ const { state } = options;
779
+ if (state?.status !== "unsupported" || options.now().getTime() - Date.parse(state.receivedAt) < DAY_MS) return state;
780
+ try {
781
+ await (await options.fetch(underBase(options.ingestBase, "v1/client-status"), {
782
+ method: "GET",
783
+ redirect: "error",
784
+ signal: AbortSignal.timeout(RECHECK_TIMEOUT_MS)
785
+ })).body?.cancel();
786
+ } catch {
787
+ return state;
788
+ }
789
+ return await readUpdateState(options.statePath, options.version) ?? state;
516
790
  }
517
- //#endregion
518
- //#region ../../packages/agent-core/src/config/plugins.ts
791
+ // ../../packages/agent-core/src/config/plugins.ts
519
792
  const NON_MARKETPLACE_ORIGINS = /* @__PURE__ */ new Set([
520
793
  "inline",
521
794
  "skills-dir",
@@ -556,8 +829,7 @@ async function collectPlugins(pluginsRoot, settingsByPrecedence, scope, guard) {
556
829
  collectMcpServers(s, (await readJsonObjectFile((0, node_path.join)(root, ".mcp.json"), guard))?.mcpServers);
557
830
  }
558
831
  }
559
- //#endregion
560
- //#region ../../packages/agent-core/src/config/collect.ts
832
+ // ../../packages/agent-core/src/config/collect.ts
561
833
  const COLLECT_BUDGET_MS = 1e3;
562
834
  async function readManagedSettings(managedDir, guard) {
563
835
  let merged = await readJsonObjectFile((0, node_path.join)(managedDir, "managed-settings.json"), guard);
@@ -691,30 +963,7 @@ async function collectConfig(options) {
691
963
  components: sortComponents(sink.components)
692
964
  };
693
965
  }
694
- //#endregion
695
- //#region ../../packages/agent-core/src/url.ts
696
- const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
697
- "localhost",
698
- "127.0.0.1",
699
- "[::1]"
700
- ]);
701
- function parseAllowedUrl(value) {
702
- if (!value) return void 0;
703
- let url;
704
- try {
705
- url = new URL(value);
706
- } catch {
707
- return;
708
- }
709
- return url.protocol === "https:" || url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname) ? url : void 0;
710
- }
711
- function underBase(base, path) {
712
- const url = new URL(base.origin);
713
- url.pathname = `${base.pathname.replace(/\/+$/, "")}/${path}`;
714
- return url;
715
- }
716
- //#endregion
717
- //#region ../../packages/agent-core/src/ingest.ts
966
+ // ../../packages/agent-core/src/ingest.ts
718
967
  const SEND_TIMEOUT_MS = 2e3;
719
968
  const INGEST_KEY_WORKSPACE = /^hf_ik_([A-Za-z0-9-]{1,128})_/;
720
969
  const workspaceIdOf = (key) => INGEST_KEY_WORKSPACE.exec(key)?.[1];
@@ -725,6 +974,7 @@ function ingestBaseFrom(endpoint) {
725
974
  ingestBase.pathname = base.pathname.replace(/\/+$/, "");
726
975
  return ingestBase;
727
976
  }
977
+ const postItem = (destination, path, item, fetchImpl, timeoutMs = SEND_TIMEOUT_MS) => postItems(destination, path, [item], fetchImpl, timeoutMs);
728
978
  async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_TIMEOUT_MS) {
729
979
  try {
730
980
  const response = await fetchImpl(underBase(destination.ingestBase, path), {
@@ -750,21 +1000,17 @@ async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_T
750
1000
  };
751
1001
  }
752
1002
  }
753
- //#endregion
754
- //#region ../../packages/agent-core/src/issue.ts
1003
+ // ../../packages/agent-core/src/issue.ts
755
1004
  const isIssueIdentifier = (value) => isToken(value, 300);
756
- //#endregion
757
- //#region ../../packages/agent-core/src/using-harnessforce.ts
758
- const usingHarnessforceBody = "---\nname: using-harnessforce\ndescription: Harnessforceのhookが入った環境でsessionを始めたとき、HarnessforceのIssue(ENG-42のような識別子)に取り組む作業を始めるとき、Harnessforceが何を記録し何を収集しないかを尋ねられたときに使う。sessionの開始時にhookがこの本文をcontextへ注入する。\n---\n\n# Harnessforceを使う\n\nこのsessionにはHarnessforceのhookが入っている。Harnessforceは、人とAIの作業をIssueに結び付けて記録する。次の規則に従う。\n\n## 何を記録するか\n\n- session registration: agent、session ID、repository、branch、commit、開始時刻、最初のprompt ID。\n- config snapshot: 利用中の設定の構成(Claude Codeだけ)。\n- OTelのテレメトリ: 利用者の設定で送るもの。\n- Issueに取り組む作業で`record-run`が記録する、Plan、Decision、Definition of Doneの充足。人が承認するまで確定しない。\n\n## 何を収集しないか\n\n- prompt、応答、toolの入出力、code diffの本文は、利用者が`--send-content`で選び、Workspaceがopt-inした場合だけ届く。既定では収集しない。\n- MCPのtoolの入力にも記録にも、これらの本文、secret、環境変数の値を含めない。\n\n## どのhookが何を送るか\n\n- SessionStart: session registrationとconfig snapshot(Codexはsession registrationだけ)。\n- UserPromptSubmit: 最初のpromptでの再送(Claude Codeだけ)。\n- hookは失敗してもsessionを止めない。送れない状態のときは、hookが警告を1行表示する。警告が出たら利用者に伝える。\n\n## どのskillとtoolをいつ使うか\n\n- IssueのIDを指定された作業、または環境変数`HARNESSFORCE_ISSUE`がある作業は、`record-run`に従い、`start_run`、`get_issue`、`record_plan`、`record_decision`、`complete_run`を使う。skillを読み込めないagentは、同じ順にtoolを呼ぶ。\n- `start_run`には、contextの`harnessforce session_id:`の行の値を渡す。複数あれば最後の値を使う。行が無ければ、session IDを推測せず、`start_run`と`complete_run`を呼ばない。\n- `/harnessforce:tune`の分析は`propose-improvements`を使う。\n- Issueが指定されていない作業では、Runの記録を始めない。\n\n## Red Flags\n\n次の考えが浮かんだら、立ち止まる。\n\n| 考え | 現実 |\n| --- | --- |\n| 小さな作業なので記録は要らない | Issueが指定された作業は、大きさによらず記録する |\n| session IDの行が無いが、推測して`start_run`を呼ぶ | 推測しない。Runがsessionと結び付かなくなる |\n| 本文を要約して渡せば記録の役に立つ | 本文はtoolに渡さない。収集しない範囲を超える |\n| 警告が出たが、気にしなくてよい | 送れていない。利用者に伝える |\n\n## 優先順位\n\n1. 利用者の指示(CLAUDE.md、AGENTS.md、直接の依頼)。利用者が記録を止めるよう指示したら、Runの記録を止める。\n2. この導入文。\n3. 既定の動作。\n\n## subagent\n\n親から特定の作業だけを委譲されたsubagentは、この導入文に従ってRunを登録しない。Runの登録は親が行う。\n".replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n+/, "").trimEnd();
759
- //#endregion
760
- //#region ../../packages/agent-core/src/session-context.ts
1005
+ // ../../packages/agent-core/src/using-harnessforce.ts
1006
+ const usingHarnessforceBody = "---\nname: using-harnessforce\ndescription: Use when starting a session in an environment with the Harnessforce hook, when starting work on an Issue, or when asked what is recorded or collected. The hook injects this text into the context at session start.\n---\n\n# Using Harnessforce\n\nThis session has the Harnessforce hook installed. Harnessforce links the work of people and AI to Issues and records it. Follow these rules.\n\n## What is recorded\n\n- Session registration: agent, session ID, repository, branch, commit, start time, and the first prompt ID.\n- Work locations: the repository and branch of each Git working tree the session works in. Paths are not sent.\n- Config snapshot: the structure of the configuration in use (Claude Code only).\n- OTel telemetry: what the user chooses to send.\n- For work on an Issue, `record-run` registers the session as a Run and closes it. It records no Plan, Decision, or Definition of Done.\n\n## What is not collected\n\n- Prompts, responses, tool input and output, and the body of code diffs arrive only when the user sends them (`--send-content` or managed settings) and the Workspace stores that category. The members the Workspace chooses can read them. `harnessforce import` never sends them.\n- Never put these contents, secrets, or environment variable values in MCP tool input or in a record.\n\n## Which hook sends what\n\n- SessionStart: session registration and the config snapshot (Codex sends session registration only).\n- UserPromptSubmit: resends on the first prompt (Claude Code only).\n- Stop: sends the work locations at the end of a turn in which they changed.\n- A failing hook never stops the session. When the hook cannot send, it shows a one-line warning. Tell the user when a warning appears.\n\n## Which skill and tool to use, and when\n\n- For work on a specified Issue ID, or when the environment variable `HARNESSFORCE_ISSUE` is set, follow `record-run` and use `start_run`, `get_issue`, and `complete_run`. An agent that cannot load the skill calls the tools in the same order.\n- Pass the value of the `harnessforce session_id:` line in the context to `start_run`. If there are several, use the last one. If the line is missing, do not guess the session ID, and do not call `start_run` or `complete_run`.\n- Use `propose-improvements` for the analysis of `/harnessforce:tune`.\n- When no Issue is specified, do not start recording a Run.\n\n## Red Flags\n\nStop when one of these thoughts comes up.\n\n| Thought | Reality |\n| --- | --- |\n| An Issue was specified, but the work is small, so recording is unnecessary | Work on a specified Issue is recorded regardless of its size |\n| The session ID line is missing, so I will guess it and call `start_run` | Do not guess. The Run would no longer be linked to the session |\n| Summarizing the content before recording would help | Do not pass content to the tool. It goes beyond what is collected |\n| A warning appeared, but I can ignore it | Sending is not working. Tell the user |\n\n## Priority\n\n1. The user's instructions (CLAUDE.md, AGENTS.md, direct requests). If the user tells you to stop recording, stop recording the Run.\n2. This introduction.\n3. Default behavior.\n\n## Subagents\n\nA subagent that was delegated only a specific task by its parent does not register a Run by following this introduction. The parent registers the Run.\n".replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---\n+/, "").trimEnd();
1007
+ // ../../packages/agent-core/src/session-context.ts
761
1008
  const sessionContextLine = (sessionId) => `harnessforce session_id: ${sessionId}`;
762
1009
  const sessionContext = (sessionId) => ({ hookSpecificOutput: {
763
1010
  hookEventName: "SessionStart",
764
1011
  additionalContext: `${sessionContextLine(sessionId)}\n\n${usingHarnessforceBody}`
765
1012
  } });
766
- //#endregion
767
- //#region ../../packages/agent-core/src/vcs.ts
1013
+ // ../../packages/agent-core/src/vcs.ts
768
1014
  async function selectRemote(cwd, git) {
769
1015
  if (await git(cwd, ["rev-parse", "--is-inside-work-tree"]) !== "true") return void 0;
770
1016
  const remotes = (await git(cwd, ["remote"]))?.split("\n") ?? [];
@@ -803,8 +1049,120 @@ async function resolveProjectRoot(cwd, git) {
803
1049
  const topLevel = await git(cwd, ["rev-parse", "--show-toplevel"]);
804
1050
  return topLevel && (0, node_path.isAbsolute)(topLevel) ? topLevel : cwd;
805
1051
  }
806
- //#endregion
807
- //#region src/input.ts
1052
+ // ../../packages/agent-core/src/work-location.ts
1053
+ const absolute = (value) => typeof value === "string" && (0, node_path.isAbsolute)(value) ? value : void 0;
1054
+ const toolInputOf = (fields) => isObject(fields.tool_input) ? fields.tool_input : {};
1055
+ const PATCH_FILE_LINE = /^\*\*\* (?:Add File|Update File|Delete File|Move to): (.+)$/;
1056
+ function patchedDirectories(command, cwd) {
1057
+ if (typeof command !== "string") return [];
1058
+ return command.split(/\r?\n/).flatMap((line) => {
1059
+ const path = PATCH_FILE_LINE.exec(line)?.[1]?.trim();
1060
+ return path ? [(0, node_path.dirname)((0, node_path.resolve)(cwd, path))] : [];
1061
+ });
1062
+ }
1063
+ /**
1064
+ * eventの入力から、作業位置を求めるdirectoryを返す。読むのは`new_cwd`、`file_path`、patchのfileの行のpathだけで、編集の
1065
+ * 本文を読まない。
1066
+ */
1067
+ function observedDirectories(agent, event, fields, cwd) {
1068
+ if (event === "stop") return [cwd];
1069
+ if (agent === "claude_code" && event === "cwd-changed") {
1070
+ const newCwd = absolute(fields.new_cwd);
1071
+ return newCwd ? [newCwd] : [];
1072
+ }
1073
+ if (event !== "post-tool-use") return [];
1074
+ const input = toolInputOf(fields);
1075
+ if (agent === "codex") return fields.tool_name === "apply_patch" ? [...new Set(patchedDirectories(input.command, cwd))] : [];
1076
+ if (fields.tool_name !== "Write" && fields.tool_name !== "Edit") return [];
1077
+ const filePath = absolute(input.file_path);
1078
+ return filePath ? [(0, node_path.dirname)(filePath)] : [];
1079
+ }
1080
+ const isDirectory = (path) => (0, node_fs_promises.stat)(path).then((found) => found.isDirectory(), () => false);
1081
+ /** directoryのworking treeの、その時点のrepositoryとbranch。repositoryかbranchを求められなければundefined。 */
1082
+ async function observePair(directory, git) {
1083
+ if (!await isDirectory(directory)) return void 0;
1084
+ const vcs = await resolveVcs(directory, git);
1085
+ return vcs?.repository && vcs.branch ? {
1086
+ repository: vcs.repository,
1087
+ branch: vcs.branch
1088
+ } : void 0;
1089
+ }
1090
+ const emptyLocationState = () => ({
1091
+ pairs: [],
1092
+ sent: [],
1093
+ unauthorized: false
1094
+ });
1095
+ const pairKey = (pair) => JSON.stringify([pair.repository, pair.branch]);
1096
+ /** 組を加える。記録済みの組と、10件に達した後の組は加えない。 */
1097
+ function withObservedPair(state, pair) {
1098
+ if (state.pairs.some((saved) => pairKey(saved) === pairKey(pair)) || state.pairs.length >= 10) return state;
1099
+ return {
1100
+ ...state,
1101
+ pairs: [...state.pairs, pair]
1102
+ };
1103
+ }
1104
+ /** 記録した組の集合が、最後に送った集合と異なる場合だけ送る組。 */
1105
+ function locationsToSend(state) {
1106
+ if (state.unauthorized || state.pairs.length === 0) return void 0;
1107
+ const sent = new Set(state.sent.map(pairKey));
1108
+ return sent.size === state.pairs.length && state.pairs.every((pair) => sent.has(pairKey(pair))) ? void 0 : state.pairs;
1109
+ }
1110
+ const isPair = (value) => isObject(value) && typeof value.repository === "string" && typeof value.branch === "string";
1111
+ async function readLocationState(path) {
1112
+ try {
1113
+ const saved = JSON.parse(await (0, node_fs_promises.readFile)(path, "utf8"));
1114
+ if (!isObject(saved) || !Array.isArray(saved.pairs) || !Array.isArray(saved.sent)) return emptyLocationState();
1115
+ return {
1116
+ pairs: saved.pairs.filter(isPair),
1117
+ sent: saved.sent.filter(isPair),
1118
+ unauthorized: saved.unauthorized === true
1119
+ };
1120
+ } catch {
1121
+ return emptyLocationState();
1122
+ }
1123
+ }
1124
+ async function writeLocationState(path, state) {
1125
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
1126
+ await (0, node_fs_promises.writeFile)(temporary, JSON.stringify(state), { mode: 384 });
1127
+ await (0, node_fs_promises.rename)(temporary, path);
1128
+ }
1129
+ const FILE_SAFE_SESSION_ID$1 = /^[A-Za-z0-9_-]+$/;
1130
+ /** Claude Codeの記録のfile。`scratchpad_dir`が無ければ記録も送信もしない。 */
1131
+ const claudeLocationStatePath = (scratchpadDir, sessionId) => FILE_SAFE_SESSION_ID$1.test(sessionId) ? (0, node_path.join)(scratchpadDir, `locations-${sessionId}.json`) : void 0;
1132
+ /** 観測したdirectoryの組を記録のfileへ加える。gitの外のdirectoryは記録しない。 */
1133
+ async function recordObservedPairs(statePath, directories, deps) {
1134
+ const pairs = await Promise.all(directories.map((directory) => observePair(directory, deps.git)));
1135
+ const before = await readLocationState(statePath);
1136
+ const after = pairs.reduce((state, pair) => pair ? withObservedPair(state, pair) : state, before);
1137
+ if (after !== before) await writeLocationState(statePath, after);
1138
+ return after;
1139
+ }
1140
+ /**
1141
+ * 記録した組の集合が最後に送った集合と異なれば`/v1/sessions/locations`へ送る。2xxなら送った集合を記録し、401なら以後送らない
1142
+ * ことを記録する。それ以外の失敗では記録を変えず、次の`Stop`が送り直しを兼ねる。
1143
+ */
1144
+ async function sendLocations(statePath, state, item, destination, fetchImpl) {
1145
+ const locations = locationsToSend(state);
1146
+ if (!locations) return { kind: "unchanged" };
1147
+ const outcome = await postItem(destination, "v1/sessions/locations", {
1148
+ agent: item.agent,
1149
+ session_id: item.sessionId,
1150
+ locations
1151
+ }, fetchImpl);
1152
+ if (outcome.kind === "accepted") await writeLocationState(statePath, {
1153
+ ...state,
1154
+ sent: locations
1155
+ });
1156
+ if (outcome.kind === "unauthorized") await writeLocationState(statePath, {
1157
+ ...state,
1158
+ unauthorized: true
1159
+ });
1160
+ return {
1161
+ kind: "sent",
1162
+ outcome
1163
+ };
1164
+ }
1165
+ // src/input.ts
808
1166
  const FILE_SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;
809
1167
  const isFileSafeSessionId = (sessionId) => FILE_SAFE_SESSION_ID.test(sessionId);
810
1168
  const token = (value) => typeof value === "string" && isToken(value) ? value : void 0;
@@ -835,8 +1193,7 @@ function parseHookInput(raw) {
835
1193
  fields
836
1194
  };
837
1195
  }
838
- //#endregion
839
- //#region src/scratchpad.ts
1196
+ // src/scratchpad.ts
840
1197
  const registrationFile = (pad) => (0, node_path.join)(pad.dir, `registration-${pad.sessionId}.json`);
841
1198
  const unauthorizedMark = (pad) => (0, node_path.join)(pad.dir, `unauthorized-${pad.sessionId}`);
842
1199
  const firstPromptSentMark = (pad) => (0, node_path.join)(pad.dir, `first-prompt-sent-${pad.sessionId}`);
@@ -863,8 +1220,7 @@ async function isFirstPromptSent(pad) {
863
1220
  async function claimFirstPrompt(pad) {
864
1221
  return (0, node_fs_promises.writeFile)(firstPromptSentMark(pad), "", { flag: "wx" }).then(() => true, () => false);
865
1222
  }
866
- //#endregion
867
- //#region src/usage/names.ts
1223
+ // src/usage/names.ts
868
1224
  const PLUGIN_COMPONENT = /^([A-Za-z0-9._-]+)@[A-Za-z0-9._-]+:(.+)$/;
869
1225
  function pluginPartsOf(identifier) {
870
1226
  const parts = identifier.plugin ? PLUGIN_COMPONENT.exec(identifier.id) : null;
@@ -900,8 +1256,7 @@ const mcpServerOfTool = (toolName, servers) => onlyOne(servers.filter((server) =
900
1256
  const prefix = toolPrefixOf(server);
901
1257
  return prefix !== void 0 && toolName.startsWith(prefix);
902
1258
  }));
903
- //#endregion
904
- //#region src/usage/count.ts
1259
+ // src/usage/count.ts
905
1260
  const COUNTING_EVENTS = [
906
1261
  "post-tool-use",
907
1262
  "post-tool-use-failure",
@@ -951,8 +1306,7 @@ function countedOf(event, fields, identifiers) {
951
1306
  } : void 0;
952
1307
  }
953
1308
  }
954
- //#endregion
955
- //#region src/usage/store.ts
1309
+ // src/usage/store.ts
956
1310
  function usageStoreOf(sessionId, env) {
957
1311
  const data = env.CLAUDE_PLUGIN_DATA;
958
1312
  if (!data || !(0, node_path.isAbsolute)(data) || !isFileSafeSessionId(sessionId)) return void 0;
@@ -1071,8 +1425,7 @@ async function removeExpiredFiles(store, now) {
1071
1425
  if (await (0, node_fs_promises.unlink)(file).then(() => true, () => false)) removed += 1;
1072
1426
  }
1073
1427
  }
1074
- //#endregion
1075
- //#region src/usage/summary.ts
1428
+ // src/usage/summary.ts
1076
1429
  const COLLECTOR_VERSION = "0.1.0";
1077
1430
  const MAX_ITEMS = 200;
1078
1431
  const byCodeUnit = (a, b) => a < b ? -1 : a > b ? 1 : 0;
@@ -1149,8 +1502,7 @@ function buildSummary(sessionId, lines) {
1149
1502
  }
1150
1503
  };
1151
1504
  }
1152
- //#endregion
1153
- //#region src/usage/send.ts
1505
+ // src/usage/send.ts
1154
1506
  async function sendSessionUsage(store, destination, deadlineMs, deps) {
1155
1507
  const state = await readState(store);
1156
1508
  if (!state || state.workspaceId !== workspaceIdOf(destination.key)) return void 0;
@@ -1203,11 +1555,13 @@ async function sendUnsentUsage(store, destination, deps) {
1203
1555
  if (outcome.kind === "accepted") await Promise.all(batch.map((sent) => writeSentLength(store, sent.sessionId, sent.state, sent.record.length)));
1204
1556
  return outcome;
1205
1557
  }
1206
- //#endregion
1207
- //#region src/hook.ts
1558
+ // src/hook.ts
1559
+ const pluginStatePath = (env) => env.CLAUDE_PLUGIN_DATA && (0, node_path.isAbsolute)(env.CLAUDE_PLUGIN_DATA) ? (0, node_path.join)(env.CLAUDE_PLUGIN_DATA, "update-status.json") : void 0;
1560
+ const cliStatePath = (homeDir) => (0, node_path.join)(homeDir, ".harnessforce", "update-status.json");
1561
+ const CLI_UNSUPPORTED_WARNING = unsupportedLine("cli");
1208
1562
  const REVOKED_KEY_MESSAGES = {
1209
- user: "送信キーが失効しています。`harnessforce init`を実行してください",
1210
- workspace: "組織の送信キーが失効しています。Workspaceの管理者に連絡してください"
1563
+ user: "Your ingest key has been revoked. Run `harnessforce init`",
1564
+ workspace: "The organization's ingest key has been revoked. Contact a Workspace administrator"
1211
1565
  };
1212
1566
  const REGISTERING_SOURCES = /* @__PURE__ */ new Set([
1213
1567
  "startup",
@@ -1216,12 +1570,25 @@ const REGISTERING_SOURCES = /* @__PURE__ */ new Set([
1216
1570
  ]);
1217
1571
  const logError = (deps) => (error) => deps.stderr(`harnessforce: ${error instanceof Error ? error.message : String(error)}\n`);
1218
1572
  const report = (deps, subject, detail) => deps.stderr(`harnessforce: ${subject} ${detail}\n`);
1573
+ const KEY_WARNINGS = {
1574
+ absent: keyMissingWarning("claude_code"),
1575
+ withheld: DESTINATION_MISMATCH_WARNING.claude_code,
1576
+ unreadable: keyUnreadableWarning("claude_code")
1577
+ };
1219
1578
  async function selectUserKey(deps, cwd) {
1220
- if (!deps.env.HARNESSFORCE_WORKSPACE_ID) return void 0;
1579
+ if (!deps.env.HARNESSFORCE_WORKSPACE_ID) return {};
1221
1580
  const read = await deps.readUserKey(cwd);
1222
- if (read.kind === "failed") deps.stderr("harnessforce: no user key in keychain or read failed\n");
1223
- return read.kind === "found" ? read.key : void 0;
1581
+ if (read.kind === "found") return { key: read.key };
1582
+ if (read.kind !== "missing") deps.stderr("harnessforce: no user key in keychain or read failed\n");
1583
+ return {
1584
+ warning: KEY_WARNINGS[read.kind],
1585
+ isCliUnsupported: read.kind === "cliUnsupported"
1586
+ };
1224
1587
  }
1588
+ const sendingNotice = (message) => message === void 0 ? void 0 : {
1589
+ kind: "sending",
1590
+ message
1591
+ };
1225
1592
  async function resolveDestination(deps, cwd, withUserKey = true) {
1226
1593
  const managed = await readManagedEnv(deps.managedDir, ["HARNESSFORCE_INGEST_KEY", "HARNESSFORCE_ENDPOINT"]);
1227
1594
  const workspaceKey = managed.HARNESSFORCE_INGEST_KEY;
@@ -1231,17 +1598,24 @@ async function resolveDestination(deps, cwd, withUserKey = true) {
1231
1598
  const ingestBase = ingestBaseFrom(endpoint);
1232
1599
  if (!ingestBase) {
1233
1600
  report(deps, "session registration", "skipped (invalid endpoint)");
1234
- return { warning: notConfigured ? notConfiguredWarning("claude_code") : INVALID_ENDPOINT_WARNING };
1601
+ return { warning: sendingNotice(notConfigured ? notConfiguredWarning("claude_code") : INVALID_ENDPOINT_WARNING) };
1235
1602
  }
1236
1603
  if (workspaceKey) return { destination: {
1237
1604
  ingestBase,
1238
1605
  key: workspaceKey,
1239
1606
  keyKind: "workspace"
1240
1607
  } };
1241
- const userKey = await selectUserKey(deps, cwd);
1608
+ const { key: userKey, warning, isCliUnsupported } = await selectUserKey(deps, cwd);
1242
1609
  if (!userKey) {
1243
1610
  report(deps, "session registration", "skipped (no ingest key)");
1244
- return notConfigured ? { warning: notConfiguredWarning("claude_code") } : {};
1611
+ if (isCliUnsupported) {
1612
+ await recheckCliStatus(deps, ingestBase);
1613
+ return { warning: {
1614
+ kind: "clientUnsupported",
1615
+ message: CLI_UNSUPPORTED_WARNING
1616
+ } };
1617
+ }
1618
+ return { warning: sendingNotice(notConfigured ? notConfiguredWarning("claude_code") : warning) };
1245
1619
  }
1246
1620
  return { destination: {
1247
1621
  ingestBase,
@@ -1249,6 +1623,24 @@ async function resolveDestination(deps, cwd, withUserKey = true) {
1249
1623
  keyKind: "user"
1250
1624
  } };
1251
1625
  }
1626
+ async function recheckCliStatus(deps, ingestBase) {
1627
+ const statePath = cliStatePath(deps.homeDir);
1628
+ const state = await readSavedUpdateState(statePath);
1629
+ if (!state) return;
1630
+ const cli = {
1631
+ component: "cli",
1632
+ version: state.version,
1633
+ dev: false
1634
+ };
1635
+ await recheckClientStatus({
1636
+ state,
1637
+ ingestBase,
1638
+ fetch: recordingFetch(deps.baseFetch, cli, statePath, deps.now),
1639
+ statePath,
1640
+ version: state.version,
1641
+ now: deps.now
1642
+ });
1643
+ }
1252
1644
  function registrationSource(destination, env) {
1253
1645
  const issue = env.HARNESSFORCE_ISSUE;
1254
1646
  return destination.keyKind === "user" && issue && isIssueIdentifier(issue) ? {
@@ -1330,25 +1722,68 @@ async function sendSessionStart(input, usage, deps) {
1330
1722
  const { destination, warning } = await resolveDestination(deps, input.cwd);
1331
1723
  if (!destination) return registers ? warning : void 0;
1332
1724
  const collected = registers || usage && !await readState(usage) ? collectComponents(input, deps) : Promise.resolve(void 0);
1333
- const [registration, snapshot, unsent] = await Promise.all([
1725
+ const isConfigured = registers && destination.keyKind === "user" ? deps.isTelemetryConfigured() : Promise.resolve(true);
1726
+ const [registration, snapshot, unsent, configured] = await Promise.all([
1334
1727
  registers ? registerSession(input, destination, deps).catch(logError(deps)) : void 0,
1335
1728
  registers ? collected.then((components) => components ? sendConfigSnapshot(input, components, destination, deps) : void 0).catch(logError(deps)) : void 0,
1336
- usage ? startUsage(usage, destination, collected, deps) : void 0
1729
+ usage ? startUsage(usage, destination, collected, deps) : void 0,
1730
+ isConfigured
1337
1731
  ]);
1338
1732
  if ([
1339
1733
  registration,
1340
1734
  snapshot,
1341
1735
  unsent
1342
- ].some((outcome) => outcome?.kind === "unauthorized")) return reportRevokedKey(destination, input.scratchpad, deps);
1343
- return registration?.kind === "failed" ? REGISTRATION_FAILED_WARNING : void 0;
1736
+ ].some((outcome) => outcome?.kind === "unauthorized")) return {
1737
+ kind: "revoked",
1738
+ message: await reportRevokedKey(destination, input.scratchpad, deps)
1739
+ };
1740
+ if (!configured) return sendingNotice(TELEMETRY_NOT_CONFIGURED_WARNING);
1741
+ return sendingNotice(registration?.kind === "failed" ? REGISTRATION_FAILED_WARNING : void 0);
1742
+ }
1743
+ async function statusCheckBase(deps) {
1744
+ const managed = await readManagedEnv(deps.managedDir, ["HARNESSFORCE_INGEST_KEY", "HARNESSFORCE_ENDPOINT"]);
1745
+ return ingestBaseFrom(managed.HARNESSFORCE_INGEST_KEY ? managed.HARNESSFORCE_ENDPOINT : deps.env.HARNESSFORCE_ENDPOINT);
1746
+ }
1747
+ /**
1748
+ * pluginの状態のfileがunsupportedなら送信を止める(client-updates.md「送信の停止」)。recheckなら、受けてから24時間以上
1749
+ * 経った状態を資格情報を送らずに確かめ直す。SessionEndは1秒の予算に収めるため確かめ直さない。
1750
+ */
1751
+ async function isPluginBlocked(deps, recheck) {
1752
+ const statePath = pluginStatePath(deps.env);
1753
+ if (!statePath) return false;
1754
+ const state = await readUpdateState(statePath, deps.client.version);
1755
+ if (state?.status !== "unsupported") return false;
1756
+ const ingestBase = recheck ? await statusCheckBase(deps) : void 0;
1757
+ return (ingestBase ? await recheckClientStatus({
1758
+ state,
1759
+ ingestBase,
1760
+ fetch: deps.fetch,
1761
+ statePath,
1762
+ version: deps.client.version,
1763
+ now: deps.now
1764
+ }) : state)?.status === "unsupported";
1765
+ }
1766
+ async function withUpdateLine(notice, input, deps) {
1767
+ if (notice && notice.kind !== "sending") return notice.message;
1768
+ const statePath = pluginStatePath(deps.env);
1769
+ const state = statePath ? await readUpdateState(statePath, deps.client.version) : void 0;
1770
+ if (state?.status === "unsupported") return updateLine("claude-code-plugin", state);
1771
+ if (notice || !state || !statePath) return notice?.message;
1772
+ return noticeLine("claude-code-plugin", statePath, state, {
1773
+ now: deps.now(),
1774
+ env: deps.env,
1775
+ isSuppressed: () => deps.isPluginAutoUpdateEffective(input.cwd)
1776
+ });
1344
1777
  }
1345
1778
  async function onSessionStart(input, deps) {
1346
1779
  const usage = usageStoreOf(input.sessionId, deps.env);
1347
1780
  const startedAt = deps.now();
1348
1781
  if (usage) await removeExpiredFiles(usage, startedAt).catch(logError(deps));
1349
- const message = await sendSessionStart(input, usage, deps).catch((error) => {
1782
+ const registers = input.source === void 0 || REGISTERING_SOURCES.has(input.source);
1783
+ const sent = await isPluginBlocked(deps, true).catch(() => false) ? void 0 : await sendSessionStart(input, usage, deps).catch((error) => {
1350
1784
  logError(deps)(error);
1351
1785
  });
1786
+ const message = registers ? await withUpdateLine(sent, input, deps).catch(() => sent?.message) : sent?.message;
1352
1787
  if (usage) await appendIfRecording(usage, {
1353
1788
  at: startedAt.toISOString(),
1354
1789
  kind: "start",
@@ -1368,6 +1803,7 @@ async function onUserPromptSubmit(input, deps) {
1368
1803
  }).catch(logError(deps));
1369
1804
  const pad = input.scratchpad;
1370
1805
  if (!input.promptId || !pad || await isMarkedUnauthorized(pad)) return;
1806
+ if (await isPluginBlocked(deps, false)) return;
1371
1807
  const saved = await loadRegistration(pad);
1372
1808
  if (!saved || await isFirstPromptSent(pad)) return;
1373
1809
  const { destination } = await resolveDestination(deps, input.cwd);
@@ -1382,6 +1818,7 @@ async function onSessionEnd(input, deps) {
1382
1818
  const usage = usageStoreOf(input.sessionId, deps.env);
1383
1819
  if (!usage) return;
1384
1820
  if (input.scratchpad && await isMarkedUnauthorized(input.scratchpad)) return;
1821
+ if (await isPluginBlocked(deps, false)) return;
1385
1822
  const { destination } = await resolveDestination(deps, input.cwd, false);
1386
1823
  if (!destination) return;
1387
1824
  const outcome = await sendSessionUsage(usage, destination, deps.processStartMs + SESSION_END_BUDGET_MS, deps);
@@ -1397,23 +1834,65 @@ const onCountingEvent = (event) => async (input, deps) => {
1397
1834
  ...counted
1398
1835
  });
1399
1836
  };
1837
+ const locationStatePathOf = (input) => input.scratchpad ? claudeLocationStatePath(input.scratchpad.dir, input.sessionId) : void 0;
1838
+ /**
1839
+ * correlation.md「sessionの作業位置」: 観測した時点のworking treeのrepositoryとbranchを記録する。`Stop`は加えて、記録した組の
1840
+ * 集合が最後に送った集合と異なれば送る。stdoutへ何も書かず、`CwdChanged`ではstderrにも書かない。
1841
+ */
1842
+ const onLocationEvent = (event) => async (input, deps) => {
1843
+ const statePath = locationStatePathOf(input);
1844
+ if (!statePath) return;
1845
+ const state = await recordObservedPairs(statePath, observedDirectories("claude_code", event, input.fields, input.cwd), deps);
1846
+ if (event !== "stop" || !input.scratchpad) return;
1847
+ if (await isMarkedUnauthorized(input.scratchpad)) return;
1848
+ if (await isPluginBlocked(deps, false)) return;
1849
+ const { destination } = await resolveDestination(deps, input.cwd);
1850
+ if (!destination) return;
1851
+ const sent = await sendLocations(statePath, state, {
1852
+ agent: "claude_code",
1853
+ sessionId: input.sessionId
1854
+ }, destination, deps.fetch);
1855
+ if (sent.kind !== "sent" || sent.outcome.kind === "accepted") return;
1856
+ if (sent.outcome.kind === "unauthorized") {
1857
+ await markUnauthorized(input.scratchpad).catch(logError(deps));
1858
+ deps.stderr("harnessforce: session location failed (unauthorized)\n");
1859
+ return;
1860
+ }
1861
+ deps.stderr(`harnessforce: session location failed (${sent.outcome.reason})\n`);
1862
+ };
1863
+ const silently = (handler) => (input, deps) => handler(input, {
1864
+ ...deps,
1865
+ stderr: () => {}
1866
+ }).catch(() => {});
1400
1867
  const HANDLERS = new Map([
1401
1868
  ["session-start", onSessionStart],
1402
1869
  ["user-prompt-submit", onUserPromptSubmit],
1403
1870
  ["session-end", onSessionEnd],
1871
+ ["cwd-changed", silently(onLocationEvent("cwd-changed"))],
1872
+ ["work-location", onLocationEvent("post-tool-use")],
1873
+ ["stop", onLocationEvent("stop")],
1404
1874
  ...COUNTING_EVENTS.map((event) => [event, onCountingEvent(event)])
1405
1875
  ]);
1406
- async function runHook(event, rawInput, deps) {
1876
+ async function runHook(event, rawInput, hookDeps) {
1407
1877
  const handler = HANDLERS.get(event);
1408
1878
  const input = parseHookInput(rawInput);
1409
1879
  if (!handler || !input) return;
1880
+ const deps = {
1881
+ ...hookDeps,
1882
+ baseFetch: hookDeps.fetch,
1883
+ fetch: recordingFetch(hookDeps.fetch, hookDeps.client, pluginStatePath(hookDeps.env), hookDeps.now)
1884
+ };
1410
1885
  await handler(input, deps).catch(logError(deps));
1411
1886
  }
1412
- //#endregion
1413
- //#region src/user-key.ts
1887
+ // src/user-key.ts
1414
1888
  const HF_TIMEOUT_MS = 1e3;
1415
1889
  const HF_OUTPUT_LIMIT_BYTES = 65536;
1416
1890
  const BEARER = /^Bearer (\S+)$/;
1891
+ const FAILURES = {
1892
+ 3: { kind: "absent" },
1893
+ 4: { kind: "withheld" },
1894
+ 5: { kind: "cliUnsupported" }
1895
+ };
1417
1896
  function parseKey(stdout) {
1418
1897
  try {
1419
1898
  const value = JSON.parse(stdout);
@@ -1438,13 +1917,17 @@ function createUserKeyReader({ platform, env, exec, processCwd, fs }) {
1438
1917
  const result = await exec(command.file, command.args, {
1439
1918
  timeoutMs: HF_TIMEOUT_MS,
1440
1919
  windowsVerbatimArguments: command.verbatim,
1441
- env
1920
+ env: {
1921
+ ...env,
1922
+ HARNESSFORCE_NO_MIGRATE: "1"
1923
+ }
1442
1924
  });
1443
1925
  const key = result.ok ? parseKey(result.stdout) : void 0;
1444
- return key ? {
1926
+ if (key) return {
1445
1927
  kind: "found",
1446
1928
  key
1447
- } : { kind: "failed" };
1929
+ };
1930
+ return result.exitCode !== void 0 && FAILURES[result.exitCode] || { kind: "unreadable" };
1448
1931
  };
1449
1932
  }
1450
1933
  const execHf = (file, args, options) => new Promise((resolve) => {
@@ -1458,11 +1941,11 @@ const execHf = (file, args, options) => new Promise((resolve) => {
1458
1941
  windowsVerbatimArguments: options.windowsVerbatimArguments
1459
1942
  }, (error, stdout) => resolve({
1460
1943
  ok: !error,
1461
- stdout
1944
+ stdout,
1945
+ ...error && !error.killed && typeof error.code === "number" ? { exitCode: error.code } : {}
1462
1946
  }));
1463
1947
  });
1464
- //#endregion
1465
- //#region src/main.ts
1948
+ // src/main.ts
1466
1949
  delete process.env.HARNESSFORCE_RUNTIME_ENV;
1467
1950
  async function readStdin() {
1468
1951
  const chunks = [];
@@ -1483,6 +1966,8 @@ async function main() {
1483
1966
  }
1484
1967
  if (relaunch.kind !== "not-needed") return;
1485
1968
  const raw = await readStdin().catch(() => "");
1969
+ const managedDir = managedDirFor(process.platform);
1970
+ const userSettingsPath = (0, node_path.join)(absoluteEnv(process.env.CLAUDE_CONFIG_DIR) ?? (0, node_path.join)((0, node_os.homedir)(), ".claude"), "settings.json");
1486
1971
  await runHook(process.argv[2] ?? "", raw, {
1487
1972
  env: process.env,
1488
1973
  now: () => /* @__PURE__ */ new Date(),
@@ -1497,14 +1982,28 @@ async function main() {
1497
1982
  stdout: (text) => process.stdout.write(text),
1498
1983
  stderr: (text) => process.stderr.write(text),
1499
1984
  homeDir: (0, node_os.homedir)(),
1500
- managedDir: managedDirFor(process.platform),
1985
+ managedDir,
1501
1986
  readUserKey: createUserKeyReader({
1502
1987
  platform: process.platform,
1503
1988
  env: process.env,
1504
1989
  exec: execHf,
1505
1990
  processCwd: process.cwd()
1991
+ }),
1992
+ isTelemetryConfigured: () => isTelemetryConfigured({
1993
+ userSettingsPath,
1994
+ managedDir
1995
+ }),
1996
+ client: {
1997
+ component: "claude-code-plugin",
1998
+ version: "0.3.0",
1999
+ dev: false
2000
+ },
2001
+ isPluginAutoUpdateEffective: (cwd) => isPluginAutoUpdateEffective({
2002
+ userSettingsPath,
2003
+ managedDir,
2004
+ projectDir: absoluteEnv(process.env.CLAUDE_PROJECT_DIR) ?? cwd,
2005
+ env: process.env
1506
2006
  })
1507
2007
  });
1508
2008
  }
1509
2009
  main();
1510
- //#endregion