@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +53 -15
- package/commands/help.md +31 -0
- package/commands/import.md +18 -0
- package/commands/run.md +25 -0
- package/commands/setup.md +30 -50
- package/commands/status.md +26 -0
- package/commands/tune.md +18 -18
- package/hooks/hooks.json +45 -1
- package/package.json +1 -1
- package/scripts/harnessforce-hook-main.cjs +703 -204
- package/scripts/harnessforce-hook.cjs +2 -1
- package/skills/propose-improvements/SKILL.md +69 -69
- package/skills/propose-improvements/scripts/summarize.mjs +59 -57
- package/skills/record-run/SKILL.md +14 -51
- package/skills/using-harnessforce/SKILL.md +34 -32
|
@@ -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
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
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
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
|
|
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
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
|
510
|
-
if (
|
|
511
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
754
|
-
//#region ../../packages/agent-core/src/issue.ts
|
|
1003
|
+
// ../../packages/agent-core/src/issue.ts
|
|
755
1004
|
const isIssueIdentifier = (value) => isToken(value, 300);
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
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
|
-
|
|
807
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1207
|
-
|
|
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: "
|
|
1210
|
-
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
|
|
1579
|
+
if (!deps.env.HARNESSFORCE_WORKSPACE_ID) return {};
|
|
1221
1580
|
const read = await deps.readUserKey(cwd);
|
|
1222
|
-
if (read.kind === "
|
|
1223
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1343
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1926
|
+
if (key) return {
|
|
1445
1927
|
kind: "found",
|
|
1446
1928
|
key
|
|
1447
|
-
}
|
|
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
|
-
|
|
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
|
|
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
|