@harnessforce/claude-code-plugin 0.1.0 → 0.2.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,16 +1,71 @@
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`;
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
+ //#endregion
32
+ //#region ../../packages/semconv/dist/repository.js
33
+ const SCHEME_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
34
+ const SCP_LIKE = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/)(.+)$/;
35
+ const HOST = /^[a-z0-9.-]+$/;
36
+ const SEGMENT = /^[A-Za-z0-9_.-]+$/;
37
+ function splitRemote(remote) {
38
+ if (SCHEME_URL.test(remote)) {
39
+ let url;
40
+ try {
41
+ url = new URL(remote);
42
+ } catch {
43
+ return;
44
+ }
45
+ return url.protocol === "file:" ? void 0 : {
46
+ host: url.hostname,
47
+ path: url.pathname
48
+ };
49
+ }
50
+ const scp = SCP_LIKE.exec(remote);
51
+ return scp?.[1] && scp[2] ? {
52
+ host: scp[1],
53
+ path: scp[2]
54
+ } : void 0;
55
+ }
56
+ function normalizeRepository(remoteUrl) {
57
+ const location = splitRemote(remoteUrl.trim());
58
+ if (!location) return void 0;
59
+ const lowerHost = location.host.toLowerCase();
60
+ const host = lowerHost === "ssh.github.com" ? "github.com" : lowerHost;
61
+ const segments = location.path.replace(/\/+$/, "").replace(/\.git$/, "").replace(/^\/+/, "").split("/");
62
+ if (segments.length !== 2 || !HOST.test(host) || !segments.every((segment) => SEGMENT.test(segment))) return void 0;
63
+ const [owner, name] = host === "github.com" ? segments.map((segment) => segment.toLowerCase()) : segments;
64
+ return `${host}/${owner}/${name}`;
65
+ }
66
+ //#endregion
67
+ //#region ../../packages/agent-core/src/object.ts
68
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14
69
  //#endregion
15
70
  //#region ../../packages/agent-core/src/config/canonical.ts
16
71
  const byCodeUnit$1 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
@@ -29,9 +84,6 @@ function sortComponents(components) {
29
84
  return [...components].sort((a, b) => byCodeUnit$1(a.kind, b.kind) || byCodeUnit$1(a.source, b.source) || byCodeUnit$1(a.id, b.id));
30
85
  }
31
86
  //#endregion
32
- //#region ../../packages/agent-core/src/object.ts
33
- const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
34
- //#endregion
35
87
  //#region ../../packages/agent-core/src/config/files.ts
36
88
  const MAX_DEPTH = 8;
37
89
  const MAX_FILE_BYTES = 1048576;
@@ -94,26 +146,175 @@ async function listDirectories(dir, guard) {
94
146
  return names;
95
147
  }
96
148
  //#endregion
149
+ //#region ../../packages/agent-core/src/config/hash-input.ts
150
+ function withoutSecrets(entry) {
151
+ const { env, headers, url, args: _args, ...rest } = entry;
152
+ const result = rest;
153
+ if (isObject(env)) result.env = Object.keys(env).sort(byCodeUnit$1);
154
+ if (isObject(headers)) result.headers = Object.keys(headers).sort(byCodeUnit$1);
155
+ const strippedUrl = typeof url === "string" ? withoutCredentials(url) : void 0;
156
+ if (strippedUrl !== void 0) result.url = strippedUrl;
157
+ return result;
158
+ }
159
+ function withoutCredentials(url) {
160
+ let parsed;
161
+ try {
162
+ parsed = new URL(url);
163
+ } catch {
164
+ return;
165
+ }
166
+ parsed.username = "";
167
+ parsed.password = "";
168
+ parsed.search = "";
169
+ parsed.hash = "";
170
+ return parsed.href;
171
+ }
172
+ const reduceHookGroup = (group) => isObject(group) && Array.isArray(group.hooks) ? {
173
+ ...group,
174
+ hooks: group.hooks.map((handler) => isObject(handler) ? withoutSecrets(handler) : handler)
175
+ } : group;
176
+ function hashInput(kind, value) {
177
+ if (kind === "mcp_server" && isObject(value)) return withoutSecrets(value);
178
+ if (kind === "hook" && Array.isArray(value)) return value.map(reduceHookGroup);
179
+ return value;
180
+ }
181
+ //#endregion
182
+ //#region ../../packages/agent-core/src/config/scope.ts
183
+ const MAX_COMPONENTS = 1e3;
184
+ var TooManyComponents = class extends Error {};
185
+ var Sink = class {
186
+ guard;
187
+ components = [];
188
+ constructor(guard) {
189
+ this.guard = guard;
190
+ }
191
+ add(component) {
192
+ if (!isToken(component.id)) return;
193
+ if (this.components.length >= MAX_COMPONENTS) throw new TooManyComponents();
194
+ this.components.push(component);
195
+ }
196
+ hasDuplicates() {
197
+ return new Set(this.components.map((c) => `${c.kind}\0${c.source}\0${c.id}`)).size !== this.components.length;
198
+ }
199
+ scope = (source, idPrefix = "", version) => {
200
+ const add = (kind, id, hash) => this.add({
201
+ kind,
202
+ source,
203
+ id: `${idPrefix}${id}`,
204
+ ...version === void 0 ? {} : { version },
205
+ hash
206
+ });
207
+ return {
208
+ guard: this.guard,
209
+ addValue: (kind, id, value) => add(kind, id, hashValue(hashInput(kind, value))),
210
+ addFile: async (kind, id, path) => {
211
+ const content = await readFileIfExists(path, this.guard);
212
+ if (content) add(kind, id, hashFileContent(content));
213
+ }
214
+ };
215
+ };
216
+ };
217
+ const absoluteEnv = (value) => value && (0, node_path.isAbsolute)(value) ? value : void 0;
218
+ const withoutExt = (path, ext) => path.slice(0, -ext.length);
219
+ const colonName = (path) => withoutExt(path, ".md").replace(/\//g, ":");
220
+ async function collectLayout(s, layout) {
221
+ 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"));
222
+ 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));
223
+ 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));
224
+ 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));
225
+ }
226
+ async function collectRules(s, base, names, rulesDir) {
227
+ for (const name of names) await s.addFile("rule", name, (0, node_path.join)(base, name));
228
+ 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));
229
+ }
230
+ function collectHooks(s, hooks) {
231
+ if (!isObject(hooks)) return;
232
+ for (const [event, value] of Object.entries(hooks)) s.addValue("hook", event, value);
233
+ }
234
+ function collectMcpServers(s, servers) {
235
+ if (!isObject(servers)) return;
236
+ for (const [name, value] of Object.entries(servers)) if (isObject(value)) s.addValue("mcp_server", name, value);
237
+ }
238
+ //#endregion
239
+ //#region ../../packages/agent-core/src/hook-warning.ts
240
+ const RESTART_FAILED_WARNING = "Could not restart the Harnessforce hook";
241
+ const INVALID_ENDPOINT_WARNING = "The Harnessforce destination is invalid. Run `harnessforce init` again";
242
+ const REGISTRATION_FAILED_WARNING = "Harnessforce session registration failed. Check your network connection";
243
+ const notConfiguredWarning = (agent) => agent === "claude_code" ? "Harnessforce sending is not configured. Run `/harnessforce:setup`" : "Harnessforce sending is not configured. Run `harnessforce init`";
244
+ const stopped = (agent) => agent === "claude_code" ? "telemetry is not being sent" : "sessions are not being registered";
245
+ const keyMissingWarning = (agent) => `Harnessforce cannot find your ingest key, so ${stopped(agent)}. Run \`harnessforce init\``;
246
+ 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\``;
247
+ const DESTINATION_MISMATCH_WARNING = {
248
+ 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",
249
+ codex: "Sessions are not being registered because the Harnessforce destination differs from the one saved by `harnessforce init`. Run `harnessforce init` again"
250
+ };
251
+ const TELEMETRY_NOT_CONFIGURED_WARNING = "Claude Code telemetry for Harnessforce is not configured. Run `harnessforce init`";
252
+ const warningOnlyOutput = (message) => `${JSON.stringify({ systemMessage: message })}\n`;
253
+ //#endregion
97
254
  //#region ../../packages/agent-core/src/managed.ts
98
255
  const MANAGED_DIRS = {
99
256
  darwin: "/Library/Application Support/ClaudeCode",
100
257
  win32: "C:\\Program Files\\ClaudeCode"
101
258
  };
102
259
  const managedDirFor = (platform) => MANAGED_DIRS[platform] ?? "/etc/claude-code";
103
- const noGuard = () => {};
260
+ const noGuard$2 = () => {};
104
261
  async function readManagedEnv(managedDir, names) {
105
262
  const dropInDir = (0, node_path.join)(managedDir, "managed-settings.d");
106
- const dropIns = (await listFiles(dropInDir, ".json", false, noGuard)).sort(byCodeUnit$1);
263
+ const dropIns = (await listFiles(dropInDir, ".json", false, noGuard$2)).sort(byCodeUnit$1);
107
264
  const files = [(0, node_path.join)(managedDir, "managed-settings.json"), ...dropIns.map((name) => (0, node_path.join)(dropInDir, name))];
108
265
  const values = Object.fromEntries(names.map((name) => [name, void 0]));
109
266
  for (const file of files) {
110
- const env = (await readJsonObjectFile(file, noGuard))?.env;
267
+ const env = (await readJsonObjectFile(file, noGuard$2))?.env;
111
268
  if (!isObject(env)) continue;
112
269
  for (const name of names) if (typeof env[name] === "string") values[name] = env[name];
113
270
  }
114
271
  return values;
115
272
  }
116
273
  //#endregion
274
+ //#region ../../packages/agent-core/src/plugin-auto-update.ts
275
+ const MARKETPLACE_NAME = "harnessforce-agent";
276
+ const MARKETPLACE_KEYS = ["extraKnownMarketplaces", "additionalMarketplaces"];
277
+ const DISABLING_VARIABLES = [
278
+ "DISABLE_UPDATES",
279
+ "DISABLE_AUTOUPDATER",
280
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"
281
+ ];
282
+ const noGuard$1 = () => {};
283
+ function marketplacesOf(settings) {
284
+ const key = MARKETPLACE_KEYS.find((name) => settings?.[name] !== void 0);
285
+ const value = key === void 0 ? void 0 : settings?.[key];
286
+ return isObject(value) ? value : void 0;
287
+ }
288
+ const declaresEntry = (settings) => MARKETPLACE_KEYS.some((key) => {
289
+ const value = settings?.[key];
290
+ return isObject(value) && value["harnessforce-agent"] !== void 0;
291
+ });
292
+ async function managedFiles(managedDir) {
293
+ const dropInDir = (0, node_path.join)(managedDir, "managed-settings.d");
294
+ const dropIns = (await listFiles(dropInDir, ".json", false, noGuard$1)).sort(byCodeUnit$1);
295
+ return [(0, node_path.join)(managedDir, "managed-settings.json"), ...dropIns.map((name) => (0, node_path.join)(dropInDir, name))];
296
+ }
297
+ /**
298
+ * Claude Codeがpluginを自動更新するとみなせるか(client-updates.md「表示」)。確かめられない場合は偽とし、outdatedを表示する。
299
+ * user settingsのentryが真で、優先度の高いfileが同じ名前のentryを持たず、自動更新を止める変数が無い場合だけ真とする。
300
+ */
301
+ async function isPluginAutoUpdateEffective(options) {
302
+ const { env } = options;
303
+ if (DISABLING_VARIABLES.some((name) => env[name] === "1") && env.FORCE_AUTOUPDATE_PLUGINS !== "1") return false;
304
+ const higher = [
305
+ ...await managedFiles(options.managedDir),
306
+ (0, node_path.join)(options.projectDir, ".claude", "settings.json"),
307
+ (0, node_path.join)(options.projectDir, ".claude", "settings.local.json")
308
+ ];
309
+ for (const file of higher) {
310
+ if (!await (0, node_fs_promises.access)(file).then(() => true, () => false)) continue;
311
+ const settings = await readJsonObjectFile(file, noGuard$1);
312
+ if (!settings || declaresEntry(settings)) return false;
313
+ }
314
+ const entry = marketplacesOf(await readJsonObjectFile(options.userSettingsPath, noGuard$1))?.[MARKETPLACE_NAME];
315
+ return isObject(entry) && entry.autoUpdate === true;
316
+ }
317
+ //#endregion
117
318
  //#region ../../packages/agent-core/src/process/lookup.ts
118
319
  const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD";
119
320
  const NPM_SHIM_MAX_BYTES = 65536;
@@ -399,120 +600,208 @@ function relaunchWithoutRuntimeVariables({ platform, env, stash, spawnSelf: star
399
600
  }));
400
601
  });
401
602
  }
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);
603
+ //#endregion
604
+ //#region ../../packages/agent-core/src/telemetry-settings.ts
605
+ const EXPECTED = {
606
+ CLAUDE_CODE_ENABLE_TELEMETRY: (value) => value === "1",
607
+ OTEL_LOGS_EXPORTER: (value) => includesOtlp(value),
608
+ OTEL_METRICS_EXPORTER: (value) => includesOtlp(value),
609
+ OTEL_TRACES_EXPORTER: (value) => includesOtlp(value),
610
+ OTEL_EXPORTER_OTLP_PROTOCOL: (value) => value === "http/protobuf"
406
611
  };
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);
612
+ const NAMES = Object.keys(EXPECTED);
613
+ const includesOtlp = (value) => value.split(",").some((exporter) => exporter.trim() === "otlp");
614
+ const noGuard = () => {};
615
+ async function isTelemetryConfigured(options) {
616
+ const managed = await readManagedEnv(options.managedDir, NAMES);
617
+ const settings = await readJsonObjectFile(options.userSettingsPath, noGuard).catch(() => void 0);
618
+ const userEnv = isObject(settings?.env) ? settings.env : {};
619
+ return NAMES.every((name) => {
620
+ const value = managed[name] ?? userEnv[name];
621
+ return typeof value === "string" && EXPECTED[name](value);
622
+ }) && typeof settings?.otelHeadersHelper === "string";
424
623
  }
425
624
  //#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
- };
625
+ //#region ../../packages/agent-core/src/url.ts
626
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
627
+ "localhost",
628
+ "127.0.0.1",
629
+ "[::1]"
630
+ ]);
631
+ function parseAllowedUrl(value) {
632
+ if (!value) return void 0;
633
+ let url;
634
+ try {
635
+ url = new URL(value);
636
+ } catch {
637
+ return;
443
638
  }
444
- const scp = SCP_LIKE.exec(remote);
445
- return scp?.[1] && scp[2] ? {
446
- host: scp[1],
447
- path: scp[2]
448
- } : void 0;
639
+ return url.protocol === "https:" || url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname) ? url : void 0;
449
640
  }
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}`;
641
+ function underBase(base, path) {
642
+ const url = new URL(base.origin);
643
+ url.pathname = `${base.pathname.replace(/\/+$/, "")}/${path}`;
644
+ return url;
459
645
  }
460
646
  //#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;
647
+ //#region ../../packages/agent-core/src/client-update.ts
648
+ const CLIENT_UPDATE_HEADER = "harnessforce-client-update";
649
+ const UPDATE_COMMANDS = {
650
+ cli: "npm install -g @harnessforce/cli@latest",
651
+ "claude-code-plugin": "claude plugin update harnessforce@harnessforce-agent"
652
+ };
653
+ const DISPLAY_NAMES = {
654
+ cli: "CLI",
655
+ "claude-code-plugin": "plugin"
656
+ };
657
+ const DAY_MS = 864e5;
658
+ const RECHECK_TIMEOUT_MS = 2e3;
659
+ const userAgentOf = (client) => `harnessforce-${client.component}${client.dev ? "-dev" : ""}/${client.version}`;
660
+ const STATUSES = /* @__PURE__ */ new Set([
661
+ "current",
662
+ "outdated",
663
+ "deprecated",
664
+ "unsupported"
665
+ ]);
666
+ const REASONS = /* @__PURE__ */ new Set(["security", "compatibility"]);
667
+ function parseClientUpdateHeader(value) {
668
+ if (!value) return void 0;
669
+ const members = /* @__PURE__ */ new Map();
670
+ for (const member of value.split(",")) {
671
+ const [key, raw] = member.trim().split("=", 2);
672
+ if (key && raw !== void 0) members.set(key, raw);
469
673
  }
470
- add(component) {
471
- if (!isToken(component.id)) return;
472
- if (this.components.length >= MAX_COMPONENTS) throw new TooManyComponents();
473
- this.components.push(component);
674
+ const status = members.get("status");
675
+ if (!status || !STATUSES.has(status)) return void 0;
676
+ const update = { status };
677
+ const latest = /^"([^"\\]*)"$/.exec(members.get("latest") ?? "")?.[1];
678
+ if (latest) update.latest = latest;
679
+ const reason = members.get("reason");
680
+ if (reason && REASONS.has(reason)) update.reason = reason;
681
+ const seconds = /^@(\d+)$/.exec(members.get("deadline") ?? "")?.[1];
682
+ if (seconds) update.deadline = (/* @__PURE__ */ new Date(Number(seconds) * 1e3)).toISOString();
683
+ return isComplete(update) ? update : void 0;
684
+ }
685
+ function isComplete(update) {
686
+ switch (update.status) {
687
+ case "outdated": return Boolean(update.latest);
688
+ case "deprecated": return Boolean(update.reason) && !Number.isNaN(Date.parse(update.deadline ?? ""));
689
+ case "unsupported": return Boolean(update.reason);
690
+ default: return true;
474
691
  }
475
- hasDuplicates() {
476
- return new Set(this.components.map((c) => `${c.kind}\0${c.source}\0${c.id}`)).size !== this.components.length;
692
+ }
693
+ function stateFrom(value, version) {
694
+ if (!isObject(value) || value.version !== version) return void 0;
695
+ if (typeof value.receivedAt !== "string") return void 0;
696
+ if (typeof value.status !== "string" || !STATUSES.has(value.status)) return void 0;
697
+ const state = value;
698
+ return isComplete(state) ? state : void 0;
699
+ }
700
+ async function readUpdateState(path, version) {
701
+ const saved = await readSavedUpdateState(path);
702
+ return saved?.version === version ? saved : void 0;
703
+ }
704
+ async function readSavedUpdateState(path) {
705
+ try {
706
+ const value = JSON.parse(await (0, node_fs_promises.readFile)(path, "utf8"));
707
+ return isObject(value) && typeof value.version === "string" ? stateFrom(value, value.version) : void 0;
708
+ } catch {
709
+ return;
477
710
  }
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
- }
711
+ }
712
+ async function writeUpdateState(path, state) {
713
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(path), {
714
+ recursive: true,
715
+ mode: 448
716
+ });
717
+ const temporary = `${path}.${(0, node_crypto.randomUUID)()}.tmp`;
718
+ await (0, node_fs_promises.writeFile)(temporary, `${JSON.stringify(state)}\n`, { mode: 384 });
719
+ await (0, node_fs_promises.rename)(temporary, path);
720
+ }
721
+ /**
722
+ * Harnessforceへの要求に`User-Agent`を付け、応答の`Harnessforce-Client-Update`を状態のfileへ保存するfetch。
723
+ * 保存に失敗しても要求の結果は変えない。statePathが無い(pluginのdata directoryが無い)場合は版の申告だけを行う。
724
+ */
725
+ function recordingFetch(fetchImpl, client, statePath, now) {
726
+ return async (url, init) => {
727
+ const headers = {
728
+ ...Object.fromEntries(new Headers(init.headers)),
729
+ "user-agent": userAgentOf(client)
493
730
  };
731
+ const response = await fetchImpl(url, {
732
+ ...init,
733
+ headers
734
+ });
735
+ const update = parseClientUpdateHeader(response.headers.get(CLIENT_UPDATE_HEADER));
736
+ if (update && statePath) {
737
+ const previous = await readUpdateState(statePath, client.version);
738
+ await writeUpdateState(statePath, {
739
+ ...update,
740
+ version: client.version,
741
+ receivedAt: now().toISOString(),
742
+ ...previous?.outdatedShownAt ? { outdatedShownAt: previous.outdatedShownAt } : {}
743
+ }).catch(() => {});
744
+ }
745
+ return response;
494
746
  };
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
747
  }
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));
748
+ const utcDate = (instant) => new Intl.DateTimeFormat("en-CA", {
749
+ timeZone: "UTC",
750
+ year: "numeric",
751
+ month: "2-digit",
752
+ day: "2-digit"
753
+ }).format(new Date(instant));
754
+ 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]}\``;
755
+ function updateLine(component, update) {
756
+ const name = DISPLAY_NAMES[component];
757
+ const command = UPDATE_COMMANDS[component];
758
+ switch (update.status) {
759
+ case "unsupported": return unsupportedLine(component);
760
+ case "deprecated": {
761
+ const stops = `stops sending on ${utcDate(update.deadline ?? "")} (00:00 UTC)`;
762
+ 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}\``;
763
+ }
764
+ case "outdated": return `Harnessforce ${name} ${update.latest} is available. Update it: \`${command}\``;
765
+ default: return;
766
+ }
508
767
  }
509
- function collectHooks(s, hooks) {
510
- if (!isObject(hooks)) return;
511
- for (const [event, value] of Object.entries(hooks)) s.addValue("hook", event, value);
768
+ function shouldShowOutdated(state, now, env) {
769
+ if (state.status !== "outdated" || env.NO_UPDATE_NOTIFIER || env.CI) return false;
770
+ const shownAt = state.outdatedShownAt ? Date.parse(state.outdatedShownAt) : NaN;
771
+ return Number.isNaN(shownAt) || now.getTime() - shownAt >= DAY_MS;
512
772
  }
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);
773
+ const markOutdatedShown = (path, state, now) => writeUpdateState(path, {
774
+ ...state,
775
+ outdatedShownAt: now.toISOString()
776
+ });
777
+ /**
778
+ * 表示する1行。outdatedは24時間に1回までとし、表示する場合はその時刻を書く。isSuppressedは、pluginを
779
+ * Claude Codeが自動更新する間など、呼び出し側がoutdatedを示さない条件。
780
+ */
781
+ async function noticeLine(component, statePath, state, options) {
782
+ if (state.status === "outdated") {
783
+ if (!shouldShowOutdated(state, options.now, options.env) || await options.isSuppressed?.()) return void 0;
784
+ await markOutdatedShown(statePath, state, options.now).catch(() => {});
785
+ }
786
+ return updateLine(component, state);
787
+ }
788
+ /**
789
+ * unsupportedを受けてから24時間以上経っていれば、資格情報を送らずに`GET /v1/client-status`で確かめ直す
790
+ * (client-updates.md「送信の停止」)。失敗した場合は状態を変えない。
791
+ */
792
+ async function recheckClientStatus(options) {
793
+ const { state } = options;
794
+ if (state?.status !== "unsupported" || options.now().getTime() - Date.parse(state.receivedAt) < DAY_MS) return state;
795
+ try {
796
+ await (await options.fetch(underBase(options.ingestBase, "v1/client-status"), {
797
+ method: "GET",
798
+ redirect: "error",
799
+ signal: AbortSignal.timeout(RECHECK_TIMEOUT_MS)
800
+ })).body?.cancel();
801
+ } catch {
802
+ return state;
803
+ }
804
+ return await readUpdateState(options.statePath, options.version) ?? state;
516
805
  }
517
806
  //#endregion
518
807
  //#region ../../packages/agent-core/src/config/plugins.ts
@@ -692,28 +981,6 @@ async function collectConfig(options) {
692
981
  };
693
982
  }
694
983
  //#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
984
  //#region ../../packages/agent-core/src/ingest.ts
718
985
  const SEND_TIMEOUT_MS = 2e3;
719
986
  const INGEST_KEY_WORKSPACE = /^hf_ik_([A-Za-z0-9-]{1,128})_/;
@@ -755,7 +1022,7 @@ async function postItems(destination, path, items, fetchImpl, timeoutMs = SEND_T
755
1022
  const isIssueIdentifier = (value) => isToken(value, 300);
756
1023
  //#endregion
757
1024
  //#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();
1025
+ 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- 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 opts in with `--send-content` and the Workspace has opted in. They are not collected by default.\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- 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();
759
1026
  //#endregion
760
1027
  //#region ../../packages/agent-core/src/session-context.ts
761
1028
  const sessionContextLine = (sessionId) => `harnessforce session_id: ${sessionId}`;
@@ -1205,9 +1472,12 @@ async function sendUnsentUsage(store, destination, deps) {
1205
1472
  }
1206
1473
  //#endregion
1207
1474
  //#region src/hook.ts
1475
+ 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;
1476
+ const cliStatePath = (homeDir) => (0, node_path.join)(homeDir, ".harnessforce", "update-status.json");
1477
+ const CLI_UNSUPPORTED_WARNING = unsupportedLine("cli");
1208
1478
  const REVOKED_KEY_MESSAGES = {
1209
- user: "送信キーが失効しています。`harnessforce init`を実行してください",
1210
- workspace: "組織の送信キーが失効しています。Workspaceの管理者に連絡してください"
1479
+ user: "Your ingest key has been revoked. Run `harnessforce init`",
1480
+ workspace: "The organization's ingest key has been revoked. Contact a Workspace administrator"
1211
1481
  };
1212
1482
  const REGISTERING_SOURCES = /* @__PURE__ */ new Set([
1213
1483
  "startup",
@@ -1216,12 +1486,25 @@ const REGISTERING_SOURCES = /* @__PURE__ */ new Set([
1216
1486
  ]);
1217
1487
  const logError = (deps) => (error) => deps.stderr(`harnessforce: ${error instanceof Error ? error.message : String(error)}\n`);
1218
1488
  const report = (deps, subject, detail) => deps.stderr(`harnessforce: ${subject} ${detail}\n`);
1489
+ const KEY_WARNINGS = {
1490
+ absent: keyMissingWarning("claude_code"),
1491
+ withheld: DESTINATION_MISMATCH_WARNING.claude_code,
1492
+ unreadable: keyUnreadableWarning("claude_code")
1493
+ };
1219
1494
  async function selectUserKey(deps, cwd) {
1220
- if (!deps.env.HARNESSFORCE_WORKSPACE_ID) return void 0;
1495
+ if (!deps.env.HARNESSFORCE_WORKSPACE_ID) return {};
1221
1496
  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;
1497
+ if (read.kind === "found") return { key: read.key };
1498
+ if (read.kind !== "missing") deps.stderr("harnessforce: no user key in keychain or read failed\n");
1499
+ return {
1500
+ warning: KEY_WARNINGS[read.kind],
1501
+ isCliUnsupported: read.kind === "cliUnsupported"
1502
+ };
1224
1503
  }
1504
+ const sendingNotice = (message) => message === void 0 ? void 0 : {
1505
+ kind: "sending",
1506
+ message
1507
+ };
1225
1508
  async function resolveDestination(deps, cwd, withUserKey = true) {
1226
1509
  const managed = await readManagedEnv(deps.managedDir, ["HARNESSFORCE_INGEST_KEY", "HARNESSFORCE_ENDPOINT"]);
1227
1510
  const workspaceKey = managed.HARNESSFORCE_INGEST_KEY;
@@ -1231,17 +1514,24 @@ async function resolveDestination(deps, cwd, withUserKey = true) {
1231
1514
  const ingestBase = ingestBaseFrom(endpoint);
1232
1515
  if (!ingestBase) {
1233
1516
  report(deps, "session registration", "skipped (invalid endpoint)");
1234
- return { warning: notConfigured ? notConfiguredWarning("claude_code") : INVALID_ENDPOINT_WARNING };
1517
+ return { warning: sendingNotice(notConfigured ? notConfiguredWarning("claude_code") : INVALID_ENDPOINT_WARNING) };
1235
1518
  }
1236
1519
  if (workspaceKey) return { destination: {
1237
1520
  ingestBase,
1238
1521
  key: workspaceKey,
1239
1522
  keyKind: "workspace"
1240
1523
  } };
1241
- const userKey = await selectUserKey(deps, cwd);
1524
+ const { key: userKey, warning, isCliUnsupported } = await selectUserKey(deps, cwd);
1242
1525
  if (!userKey) {
1243
1526
  report(deps, "session registration", "skipped (no ingest key)");
1244
- return notConfigured ? { warning: notConfiguredWarning("claude_code") } : {};
1527
+ if (isCliUnsupported) {
1528
+ await recheckCliStatus(deps, ingestBase);
1529
+ return { warning: {
1530
+ kind: "clientUnsupported",
1531
+ message: CLI_UNSUPPORTED_WARNING
1532
+ } };
1533
+ }
1534
+ return { warning: sendingNotice(notConfigured ? notConfiguredWarning("claude_code") : warning) };
1245
1535
  }
1246
1536
  return { destination: {
1247
1537
  ingestBase,
@@ -1249,6 +1539,24 @@ async function resolveDestination(deps, cwd, withUserKey = true) {
1249
1539
  keyKind: "user"
1250
1540
  } };
1251
1541
  }
1542
+ async function recheckCliStatus(deps, ingestBase) {
1543
+ const statePath = cliStatePath(deps.homeDir);
1544
+ const state = await readSavedUpdateState(statePath);
1545
+ if (!state) return;
1546
+ const cli = {
1547
+ component: "cli",
1548
+ version: state.version,
1549
+ dev: false
1550
+ };
1551
+ await recheckClientStatus({
1552
+ state,
1553
+ ingestBase,
1554
+ fetch: recordingFetch(deps.baseFetch, cli, statePath, deps.now),
1555
+ statePath,
1556
+ version: state.version,
1557
+ now: deps.now
1558
+ });
1559
+ }
1252
1560
  function registrationSource(destination, env) {
1253
1561
  const issue = env.HARNESSFORCE_ISSUE;
1254
1562
  return destination.keyKind === "user" && issue && isIssueIdentifier(issue) ? {
@@ -1330,25 +1638,68 @@ async function sendSessionStart(input, usage, deps) {
1330
1638
  const { destination, warning } = await resolveDestination(deps, input.cwd);
1331
1639
  if (!destination) return registers ? warning : void 0;
1332
1640
  const collected = registers || usage && !await readState(usage) ? collectComponents(input, deps) : Promise.resolve(void 0);
1333
- const [registration, snapshot, unsent] = await Promise.all([
1641
+ const isConfigured = registers && destination.keyKind === "user" ? deps.isTelemetryConfigured() : Promise.resolve(true);
1642
+ const [registration, snapshot, unsent, configured] = await Promise.all([
1334
1643
  registers ? registerSession(input, destination, deps).catch(logError(deps)) : void 0,
1335
1644
  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
1645
+ usage ? startUsage(usage, destination, collected, deps) : void 0,
1646
+ isConfigured
1337
1647
  ]);
1338
1648
  if ([
1339
1649
  registration,
1340
1650
  snapshot,
1341
1651
  unsent
1342
- ].some((outcome) => outcome?.kind === "unauthorized")) return reportRevokedKey(destination, input.scratchpad, deps);
1343
- return registration?.kind === "failed" ? REGISTRATION_FAILED_WARNING : void 0;
1652
+ ].some((outcome) => outcome?.kind === "unauthorized")) return {
1653
+ kind: "revoked",
1654
+ message: await reportRevokedKey(destination, input.scratchpad, deps)
1655
+ };
1656
+ if (!configured) return sendingNotice(TELEMETRY_NOT_CONFIGURED_WARNING);
1657
+ return sendingNotice(registration?.kind === "failed" ? REGISTRATION_FAILED_WARNING : void 0);
1658
+ }
1659
+ async function statusCheckBase(deps) {
1660
+ const managed = await readManagedEnv(deps.managedDir, ["HARNESSFORCE_INGEST_KEY", "HARNESSFORCE_ENDPOINT"]);
1661
+ return ingestBaseFrom(managed.HARNESSFORCE_INGEST_KEY ? managed.HARNESSFORCE_ENDPOINT : deps.env.HARNESSFORCE_ENDPOINT);
1662
+ }
1663
+ /**
1664
+ * pluginの状態のfileがunsupportedなら送信を止める(client-updates.md「送信の停止」)。recheckなら、受けてから24時間以上
1665
+ * 経った状態を資格情報を送らずに確かめ直す。SessionEndは1秒の予算に収めるため確かめ直さない。
1666
+ */
1667
+ async function isPluginBlocked(deps, recheck) {
1668
+ const statePath = pluginStatePath(deps.env);
1669
+ if (!statePath) return false;
1670
+ const state = await readUpdateState(statePath, deps.client.version);
1671
+ if (state?.status !== "unsupported") return false;
1672
+ const ingestBase = recheck ? await statusCheckBase(deps) : void 0;
1673
+ return (ingestBase ? await recheckClientStatus({
1674
+ state,
1675
+ ingestBase,
1676
+ fetch: deps.fetch,
1677
+ statePath,
1678
+ version: deps.client.version,
1679
+ now: deps.now
1680
+ }) : state)?.status === "unsupported";
1681
+ }
1682
+ async function withUpdateLine(notice, input, deps) {
1683
+ if (notice && notice.kind !== "sending") return notice.message;
1684
+ const statePath = pluginStatePath(deps.env);
1685
+ const state = statePath ? await readUpdateState(statePath, deps.client.version) : void 0;
1686
+ if (state?.status === "unsupported") return updateLine("claude-code-plugin", state);
1687
+ if (notice || !state || !statePath) return notice?.message;
1688
+ return noticeLine("claude-code-plugin", statePath, state, {
1689
+ now: deps.now(),
1690
+ env: deps.env,
1691
+ isSuppressed: () => deps.isPluginAutoUpdateEffective(input.cwd)
1692
+ });
1344
1693
  }
1345
1694
  async function onSessionStart(input, deps) {
1346
1695
  const usage = usageStoreOf(input.sessionId, deps.env);
1347
1696
  const startedAt = deps.now();
1348
1697
  if (usage) await removeExpiredFiles(usage, startedAt).catch(logError(deps));
1349
- const message = await sendSessionStart(input, usage, deps).catch((error) => {
1698
+ const registers = input.source === void 0 || REGISTERING_SOURCES.has(input.source);
1699
+ const sent = await isPluginBlocked(deps, true).catch(() => false) ? void 0 : await sendSessionStart(input, usage, deps).catch((error) => {
1350
1700
  logError(deps)(error);
1351
1701
  });
1702
+ const message = registers ? await withUpdateLine(sent, input, deps).catch(() => sent?.message) : sent?.message;
1352
1703
  if (usage) await appendIfRecording(usage, {
1353
1704
  at: startedAt.toISOString(),
1354
1705
  kind: "start",
@@ -1368,6 +1719,7 @@ async function onUserPromptSubmit(input, deps) {
1368
1719
  }).catch(logError(deps));
1369
1720
  const pad = input.scratchpad;
1370
1721
  if (!input.promptId || !pad || await isMarkedUnauthorized(pad)) return;
1722
+ if (await isPluginBlocked(deps, false)) return;
1371
1723
  const saved = await loadRegistration(pad);
1372
1724
  if (!saved || await isFirstPromptSent(pad)) return;
1373
1725
  const { destination } = await resolveDestination(deps, input.cwd);
@@ -1382,6 +1734,7 @@ async function onSessionEnd(input, deps) {
1382
1734
  const usage = usageStoreOf(input.sessionId, deps.env);
1383
1735
  if (!usage) return;
1384
1736
  if (input.scratchpad && await isMarkedUnauthorized(input.scratchpad)) return;
1737
+ if (await isPluginBlocked(deps, false)) return;
1385
1738
  const { destination } = await resolveDestination(deps, input.cwd, false);
1386
1739
  if (!destination) return;
1387
1740
  const outcome = await sendSessionUsage(usage, destination, deps.processStartMs + SESSION_END_BUDGET_MS, deps);
@@ -1403,10 +1756,15 @@ const HANDLERS = new Map([
1403
1756
  ["session-end", onSessionEnd],
1404
1757
  ...COUNTING_EVENTS.map((event) => [event, onCountingEvent(event)])
1405
1758
  ]);
1406
- async function runHook(event, rawInput, deps) {
1759
+ async function runHook(event, rawInput, hookDeps) {
1407
1760
  const handler = HANDLERS.get(event);
1408
1761
  const input = parseHookInput(rawInput);
1409
1762
  if (!handler || !input) return;
1763
+ const deps = {
1764
+ ...hookDeps,
1765
+ baseFetch: hookDeps.fetch,
1766
+ fetch: recordingFetch(hookDeps.fetch, hookDeps.client, pluginStatePath(hookDeps.env), hookDeps.now)
1767
+ };
1410
1768
  await handler(input, deps).catch(logError(deps));
1411
1769
  }
1412
1770
  //#endregion
@@ -1414,6 +1772,11 @@ async function runHook(event, rawInput, deps) {
1414
1772
  const HF_TIMEOUT_MS = 1e3;
1415
1773
  const HF_OUTPUT_LIMIT_BYTES = 65536;
1416
1774
  const BEARER = /^Bearer (\S+)$/;
1775
+ const FAILURES = {
1776
+ 3: { kind: "absent" },
1777
+ 4: { kind: "withheld" },
1778
+ 5: { kind: "cliUnsupported" }
1779
+ };
1417
1780
  function parseKey(stdout) {
1418
1781
  try {
1419
1782
  const value = JSON.parse(stdout);
@@ -1438,13 +1801,17 @@ function createUserKeyReader({ platform, env, exec, processCwd, fs }) {
1438
1801
  const result = await exec(command.file, command.args, {
1439
1802
  timeoutMs: HF_TIMEOUT_MS,
1440
1803
  windowsVerbatimArguments: command.verbatim,
1441
- env
1804
+ env: {
1805
+ ...env,
1806
+ HARNESSFORCE_NO_MIGRATE: "1"
1807
+ }
1442
1808
  });
1443
1809
  const key = result.ok ? parseKey(result.stdout) : void 0;
1444
- return key ? {
1810
+ if (key) return {
1445
1811
  kind: "found",
1446
1812
  key
1447
- } : { kind: "failed" };
1813
+ };
1814
+ return result.exitCode !== void 0 && FAILURES[result.exitCode] || { kind: "unreadable" };
1448
1815
  };
1449
1816
  }
1450
1817
  const execHf = (file, args, options) => new Promise((resolve) => {
@@ -1458,7 +1825,8 @@ const execHf = (file, args, options) => new Promise((resolve) => {
1458
1825
  windowsVerbatimArguments: options.windowsVerbatimArguments
1459
1826
  }, (error, stdout) => resolve({
1460
1827
  ok: !error,
1461
- stdout
1828
+ stdout,
1829
+ ...error && !error.killed && typeof error.code === "number" ? { exitCode: error.code } : {}
1462
1830
  }));
1463
1831
  });
1464
1832
  //#endregion
@@ -1483,6 +1851,8 @@ async function main() {
1483
1851
  }
1484
1852
  if (relaunch.kind !== "not-needed") return;
1485
1853
  const raw = await readStdin().catch(() => "");
1854
+ const managedDir = managedDirFor(process.platform);
1855
+ const userSettingsPath = (0, node_path.join)(absoluteEnv(process.env.CLAUDE_CONFIG_DIR) ?? (0, node_path.join)((0, node_os.homedir)(), ".claude"), "settings.json");
1486
1856
  await runHook(process.argv[2] ?? "", raw, {
1487
1857
  env: process.env,
1488
1858
  now: () => /* @__PURE__ */ new Date(),
@@ -1497,12 +1867,27 @@ async function main() {
1497
1867
  stdout: (text) => process.stdout.write(text),
1498
1868
  stderr: (text) => process.stderr.write(text),
1499
1869
  homeDir: (0, node_os.homedir)(),
1500
- managedDir: managedDirFor(process.platform),
1870
+ managedDir,
1501
1871
  readUserKey: createUserKeyReader({
1502
1872
  platform: process.platform,
1503
1873
  env: process.env,
1504
1874
  exec: execHf,
1505
1875
  processCwd: process.cwd()
1876
+ }),
1877
+ isTelemetryConfigured: () => isTelemetryConfigured({
1878
+ userSettingsPath,
1879
+ managedDir
1880
+ }),
1881
+ client: {
1882
+ component: "claude-code-plugin",
1883
+ version: "0.2.0",
1884
+ dev: false
1885
+ },
1886
+ isPluginAutoUpdateEffective: (cwd) => isPluginAutoUpdateEffective({
1887
+ userSettingsPath,
1888
+ managedDir,
1889
+ projectDir: absoluteEnv(process.env.CLAUDE_PROJECT_DIR) ?? cwd,
1890
+ env: process.env
1506
1891
  })
1507
1892
  });
1508
1893
  }