@x1aoye/conflux-oc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1394 @@
1
+ // src/config.ts
2
+ import { readFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ import { parse } from "jsonc-parser";
6
+ var BUILTIN_DEFAULTS = {
7
+ layers: [
8
+ { name: "platform", priority: 0, storage: "transform-only", inject: "always" },
9
+ { name: "machine", priority: 1, storage: { type: "local", path: "machines/{id}.json" }, inject: "summary-only" },
10
+ { name: "user", priority: 2, storage: { type: "local", path: "users/{name}.json" }, inject: "always" },
11
+ { name: "project", priority: 3, storage: { type: "local", path: ".opencode/skills/" }, inject: "on-demand" }
12
+ ],
13
+ behavior_rules: [
14
+ {
15
+ id: "precheck-runtime",
16
+ trigger: "before:build_run_test",
17
+ rule: "Before build/run/test: read Project Runtime Requirements first, then check Machine toolchain paths, auto-prepend version switching commands",
18
+ enabled: true
19
+ },
20
+ {
21
+ id: "knowledge-routing-cross-branch",
22
+ trigger: "on:note_discovery",
23
+ rule: "Core architecture/interface contracts/design decisions \u2192 _shared/; current branch progress/implementation details \u2192 SKILL.md",
24
+ condition: "is_git_project",
25
+ enabled: true
26
+ },
27
+ {
28
+ id: "knowledge-source-routing",
29
+ trigger: "on:note_discovery",
30
+ rule: "Only tool output results go to Machine/Platform layers; user assumptions never enter any layer",
31
+ enabled: true
32
+ }
33
+ ]
34
+ };
35
+ var CONFIG_FILE_NAME = "conflux-oc.jsonc";
36
+ function configSearchPaths(projectRoot) {
37
+ const paths = [];
38
+ paths.push(join(projectRoot, ".opencode", CONFIG_FILE_NAME));
39
+ const customDir = process.env.OPENCODE_CONFIG_DIR;
40
+ if (customDir) {
41
+ paths.push(join(customDir, CONFIG_FILE_NAME));
42
+ }
43
+ paths.push(join(homedir(), ".config", "opencode", CONFIG_FILE_NAME));
44
+ return paths;
45
+ }
46
+ function resolvePluginDataDir() {
47
+ const configDir = process.env.OPENCODE_CONFIG_DIR;
48
+ if (configDir) {
49
+ return join(configDir, "plugins", "conflux-oc");
50
+ }
51
+ return join(homedir(), ".config", "opencode", "plugins", "conflux-oc");
52
+ }
53
+ function loadPluginConfig(projectRoot) {
54
+ const base = projectRoot || process.cwd();
55
+ for (const p of configSearchPaths(base)) {
56
+ try {
57
+ const raw = readFileSync(p, "utf-8");
58
+ const parsed = parse(raw);
59
+ if (parsed?.layers && parsed?.behavior_rules) {
60
+ return parsed;
61
+ }
62
+ } catch {
63
+ continue;
64
+ }
65
+ }
66
+ return { ...BUILTIN_DEFAULTS, layers: BUILTIN_DEFAULTS.layers.map((l) => ({ ...l })) };
67
+ }
68
+ function resolveLayerStorage(layer, pluginDataDir, projectRoot) {
69
+ if (typeof layer.storage === "string") {
70
+ return { type: "none", basePath: "", template: "" };
71
+ }
72
+ if (layer.storage.type === "local") {
73
+ const isProjectScope = layer.storage.path.startsWith(".opencode");
74
+ const basePath = isProjectScope ? projectRoot : pluginDataDir;
75
+ return {
76
+ type: "local",
77
+ basePath,
78
+ template: layer.storage.path
79
+ };
80
+ }
81
+ return { type: "none", basePath: "", template: "" };
82
+ }
83
+ function resolvePluginConfig(projectRoot) {
84
+ const base = projectRoot || process.cwd();
85
+ const config = loadPluginConfig(base);
86
+ const pluginDataDir = resolvePluginDataDir();
87
+ const resolvedStorages = {};
88
+ for (const layer of config.layers) {
89
+ resolvedStorages[layer.name] = resolveLayerStorage(layer, pluginDataDir, base);
90
+ }
91
+ return {
92
+ layers: config.layers,
93
+ behavior_rules: config.behavior_rules,
94
+ pluginDataDir,
95
+ projectRoot: base,
96
+ resolvedStorages
97
+ };
98
+ }
99
+
100
+ // src/layers/registry.ts
101
+ var LayerRegistry = class {
102
+ layers;
103
+ constructor(layers) {
104
+ this.layers = [...layers].sort((a, b) => a.priority - b.priority);
105
+ }
106
+ getByPriority() {
107
+ return this.layers;
108
+ }
109
+ getByName(name) {
110
+ return this.layers.find((l) => l.name === name);
111
+ }
112
+ add(layer) {
113
+ this.layers.push(layer);
114
+ this.layers.sort((a, b) => a.priority - b.priority);
115
+ }
116
+ };
117
+
118
+ // src/detection/platform.ts
119
+ import { platform, release, arch, type } from "os";
120
+ import { env } from "process";
121
+ function detectOS() {
122
+ const name = type();
123
+ const releaseStr = release();
124
+ switch (platform()) {
125
+ case "darwin":
126
+ return `macOS (${osxReleaseName(releaseStr)})`;
127
+ case "linux":
128
+ return `Linux${releaseStr ? ` (${releaseStr})` : ""}`;
129
+ case "win32":
130
+ return `Windows${releaseStr ? ` (${releaseStr})` : ""}`;
131
+ default:
132
+ return `${name} ${releaseStr}`;
133
+ }
134
+ }
135
+ function osxReleaseName(release2) {
136
+ const major = Number.parseInt(release2.split(".")[0]);
137
+ const names = {
138
+ 20: "Big Sur",
139
+ 21: "Monterey",
140
+ 22: "Ventura",
141
+ 23: "Sonoma",
142
+ 24: "Sequoia",
143
+ 25: "Sequoia"
144
+ };
145
+ return names[major] ? `${names[major]} ${release2}` : release2;
146
+ }
147
+ function detectShell() {
148
+ const shellPath = env.SHELL || env.ComSpec || "unknown";
149
+ const name = shellPath.split("/").pop()?.split("\\").pop() || shellPath;
150
+ return `${name} (${shellPath})`;
151
+ }
152
+ function detectArch() {
153
+ const archMap = {
154
+ x64: "x86_64",
155
+ arm64: "aarch64",
156
+ ia32: "x86"
157
+ };
158
+ return archMap[arch()] || arch();
159
+ }
160
+ function detectPlatform() {
161
+ return {
162
+ os: detectOS(),
163
+ shell: detectShell(),
164
+ arch: detectArch()
165
+ };
166
+ }
167
+
168
+ // src/layers/platform-layer.ts
169
+ function getPlatformContext(_config) {
170
+ return detectPlatform();
171
+ }
172
+ function formatPlatformContext(platform4) {
173
+ return `OS: ${platform4.os}
174
+ Shell: ${platform4.shell}
175
+ Arch: ${platform4.arch}`;
176
+ }
177
+
178
+ // src/layers/machine-layer.ts
179
+ import { existsSync, readFileSync as readFileSync3, writeFileSync, mkdirSync } from "fs";
180
+ import { join as join2, dirname } from "path";
181
+ import { hostname as hostname2 } from "os";
182
+
183
+ // src/detection/machine-id.ts
184
+ import { readFileSync as readFileSync2 } from "fs";
185
+ import { execSync } from "child_process";
186
+ import { hostname, platform as platform2 } from "os";
187
+ function tryDMISerial() {
188
+ try {
189
+ const paths = [
190
+ "/sys/devices/virtual/dmi/id/product_name",
191
+ "/sys/devices/virtual/dmi/id/product_serial"
192
+ ];
193
+ for (const p of paths) {
194
+ try {
195
+ const content = readFileSync2(p, "utf-8").trim();
196
+ if (content && content !== "Not Specified" && content !== "System Product Name") {
197
+ return content;
198
+ }
199
+ } catch {
200
+ continue;
201
+ }
202
+ }
203
+ } catch {
204
+ }
205
+ return null;
206
+ }
207
+ function tryMachineID() {
208
+ try {
209
+ return readFileSync2("/etc/machine-id", "utf-8").trim().slice(0, 8) || null;
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+ function tryHostnameMAC() {
215
+ const hn = hostname();
216
+ try {
217
+ const isWin = platform2() === "win32";
218
+ const cmd = isWin ? 'powershell -Command "Get-NetAdapter | Select-Object -ExpandProperty MacAddress"' : "ifconfig 2>/dev/null || ip link 2>/dev/null";
219
+ const output = execSync(cmd, { encoding: "utf-8", timeout: 5e3 });
220
+ const macs = output.match(/([0-9A-Fa-f]{2}([:-][0-9A-Fa-f]{2}){5})/g);
221
+ if (macs && macs.length > 0) {
222
+ return `${hn}-${macs[0].replace(/[:-]/g, "").slice(0, 6)}`;
223
+ }
224
+ return hn;
225
+ } catch {
226
+ return hn;
227
+ }
228
+ }
229
+ function tryWindowsMachineId() {
230
+ try {
231
+ const output = execSync(
232
+ 'powershell -Command "Get-CimInstance -Class Win32_ComputerSystemProduct | Select-Object -ExpandProperty UUID"',
233
+ { encoding: "utf-8", timeout: 5e3 }
234
+ );
235
+ const trimmed = output.trim();
236
+ return trimmed && trimmed !== "00000000-0000-0000-0000-000000000000" ? trimmed : null;
237
+ } catch {
238
+ return null;
239
+ }
240
+ }
241
+ function identifyMachine() {
242
+ const isWin = platform2() === "win32";
243
+ if (!isWin) {
244
+ const dmi = tryDMISerial();
245
+ if (dmi) return { id: dmi, method: "dmi" };
246
+ const machineId = tryMachineID();
247
+ if (machineId) return { id: machineId, method: "machine-id" };
248
+ }
249
+ if (isWin) {
250
+ const winId = tryWindowsMachineId();
251
+ if (winId) return { id: winId, method: "hostname-mac" };
252
+ }
253
+ return { id: tryHostnameMAC(), method: "hostname-mac" };
254
+ }
255
+ function sanitizeId(id) {
256
+ return id.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").slice(0, 64);
257
+ }
258
+
259
+ // src/layers/machine-layer.ts
260
+ function resolveMachineId() {
261
+ try {
262
+ const identity = identifyMachine();
263
+ const id = sanitizeId(identity.id);
264
+ return { id, method: identity.method };
265
+ } catch {
266
+ return {
267
+ id: sanitizeId(`fallback-${hostname2()}`),
268
+ method: "hostname-mac"
269
+ };
270
+ }
271
+ }
272
+ var _cachedId = null;
273
+ function getMachineId() {
274
+ if (!_cachedId) {
275
+ _cachedId = resolveMachineId();
276
+ }
277
+ return _cachedId;
278
+ }
279
+ function machineProfilePath(config) {
280
+ const storage = config.resolvedStorages.machine;
281
+ if (storage.type !== "local") return "";
282
+ const { id } = getMachineId();
283
+ return join2(storage.basePath, storage.template.replace("{id}", id));
284
+ }
285
+ function loadMachineProfile(config) {
286
+ const path = machineProfilePath(config);
287
+ if (!path) return null;
288
+ try {
289
+ if (!existsSync(path)) return null;
290
+ const raw = readFileSync3(path, "utf-8");
291
+ return JSON.parse(raw);
292
+ } catch {
293
+ return null;
294
+ }
295
+ }
296
+ function saveMachineProfile(config, profile) {
297
+ const path = machineProfilePath(config);
298
+ if (!path) return;
299
+ mkdirSync(dirname(path), { recursive: true });
300
+ profile.last_updated = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
301
+ writeFileSync(path, JSON.stringify(profile, null, 2), "utf-8");
302
+ }
303
+ function createMachineProfile(config, domains) {
304
+ const { id, method } = getMachineId();
305
+ return {
306
+ machine_id: id,
307
+ id_method: method,
308
+ hostname: hostname2(),
309
+ last_updated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
310
+ domains
311
+ };
312
+ }
313
+
314
+ // src/detection/project.ts
315
+ import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
316
+ import { join as join3 } from "path";
317
+ import { parse as parseYaml } from "yaml";
318
+ var DETECTORS = {
319
+ java_maven: {
320
+ file: "pom.xml",
321
+ parse: (content) => {
322
+ const javaMatch = content.match(/<java\.version>([^<]+)<\/java\.version>/) || content.match(/<maven\.compiler\.source>([^<]+)<\/maven\.compiler\.source>/);
323
+ if (!javaMatch) return null;
324
+ return {
325
+ languages: ["java"],
326
+ versions: { java: javaMatch[1] },
327
+ build: "mvn clean compile",
328
+ test: "mvn test"
329
+ };
330
+ }
331
+ },
332
+ java_gradle: {
333
+ file: "build.gradle",
334
+ parse: (content) => {
335
+ const match = content.match(/sourceCompatibility\s*=\s*['"]?(\d+\.?\d*)['"]?/);
336
+ if (!match) return null;
337
+ return {
338
+ languages: ["java"],
339
+ versions: { java: match[1] },
340
+ build: "gradle build",
341
+ test: "gradle test"
342
+ };
343
+ }
344
+ },
345
+ java_gradle_kts: {
346
+ file: "build.gradle.kts",
347
+ parse: (content) => {
348
+ const match = content.match(/JavaVersion\.VERSION_(\d+\.?\d*)/) || content.match(/java\.toolchain\.languageVersion\.set\(JavaLanguageVersion\.of\((\d+\.?\d*)\)\)/);
349
+ if (!match) return null;
350
+ return {
351
+ languages: ["java"],
352
+ versions: { java: match[1] },
353
+ build: "gradle build",
354
+ test: "gradle test"
355
+ };
356
+ }
357
+ },
358
+ node: {
359
+ file: "package.json",
360
+ parse: (content) => {
361
+ const pkg = JSON.parse(content);
362
+ const scripts = pkg.scripts || {};
363
+ return {
364
+ languages: ["javascript", "typescript"],
365
+ versions: { node: "unknown" },
366
+ build: scripts.build || "npm run build",
367
+ test: scripts.test || "npm test"
368
+ };
369
+ }
370
+ },
371
+ go: {
372
+ file: "go.mod",
373
+ parse: (content) => {
374
+ const match = content.match(/^go (\d+\.\d+\.?\d*)/m);
375
+ return {
376
+ languages: ["go"],
377
+ versions: { go: match?.[1] || "unknown" },
378
+ build: "go build ./...",
379
+ test: "go test ./..."
380
+ };
381
+ }
382
+ },
383
+ rust: {
384
+ file: "Cargo.toml",
385
+ parse: () => ({
386
+ languages: ["rust"],
387
+ versions: { rust: "unknown" },
388
+ build: "cargo build",
389
+ test: "cargo test"
390
+ })
391
+ }
392
+ };
393
+ function runtimeRequirementsPath(projectRoot) {
394
+ return join3(projectRoot, ".opencode", "skills", "_shared", "runtime-requirements.yaml");
395
+ }
396
+ function readRuntimeRequirements(projectRoot) {
397
+ const path = runtimeRequirementsPath(projectRoot);
398
+ try {
399
+ if (!existsSync2(path)) return null;
400
+ const raw = readFileSync4(path, "utf-8");
401
+ return parseYaml(raw);
402
+ } catch {
403
+ return null;
404
+ }
405
+ }
406
+ function detectProjectRuntime(projectRoot) {
407
+ const result = {
408
+ languages: [],
409
+ versions: {},
410
+ build: "",
411
+ test: ""
412
+ };
413
+ for (const [, detector] of Object.entries(DETECTORS)) {
414
+ const filePath = join3(projectRoot, detector.file);
415
+ if (!existsSync2(filePath)) continue;
416
+ try {
417
+ const content = readFileSync4(filePath, "utf-8");
418
+ const parsed = detector.parse(content);
419
+ if (parsed) {
420
+ result.languages.push(...parsed.languages);
421
+ Object.assign(result.versions, parsed.versions);
422
+ result.build = parsed.build || result.build;
423
+ result.test = parsed.test || result.test;
424
+ }
425
+ } catch {
426
+ continue;
427
+ }
428
+ }
429
+ result.languages = [...new Set(result.languages)];
430
+ return result;
431
+ }
432
+
433
+ // src/layers/project-layer.ts
434
+ function getProjectRuntime(config) {
435
+ return detectProjectRuntime(config.projectRoot);
436
+ }
437
+ function getRecordedRequirements(config) {
438
+ return readRuntimeRequirements(config.projectRoot);
439
+ }
440
+ function diffRuntimeVersions(current, recorded) {
441
+ const warnings = [];
442
+ if (!recorded) return warnings;
443
+ for (const [lang, currentVersion] of Object.entries(current.versions)) {
444
+ const recordedVersion = recorded[lang]?.version;
445
+ if (recordedVersion && !currentVersion.includes(recordedVersion)) {
446
+ warnings.push(
447
+ `Project ${lang} version is ${currentVersion}, but recorded Runtime Requirements require ${recordedVersion}.`
448
+ );
449
+ }
450
+ }
451
+ return warnings;
452
+ }
453
+
454
+ // src/layers/user-layer.ts
455
+ import { readFileSync as readFileSync5 } from "fs";
456
+ import { join as join4 } from "path";
457
+
458
+ // src/detection/language.ts
459
+ var LANGUAGE_MAP = {
460
+ zh: "\u6280\u672F\u89E3\u91CA\u7528\u4E2D\u6587\uFF0C\u547D\u4EE4\u8F93\u51FA\u4FDD\u7559\u539F\u6587",
461
+ ja: "\u6280\u8853\u8AAC\u660E\u306F\u65E5\u672C\u8A9E\u3001\u30B3\u30DE\u30F3\u30C9\u51FA\u529B\u306F\u539F\u6587\u306E\u307E\u307E",
462
+ ko: "\uAE30\uC220 \uC124\uBA85\uC740 \uD55C\uAD6D\uC5B4, \uBA85\uB839\uC5B4 \uCD9C\uB825\uC740 \uC6D0\uBB38 \uC720\uC9C0",
463
+ de: "Technische Erkl\xE4rungen auf Deutsch, Befehlsausgabe im Original",
464
+ fr: "Explications techniques en fran\xE7ais, sortie des commandes en original",
465
+ es: "Explicaciones t\xE9cnicas en espa\xF1ol, salida de comandos en original"
466
+ };
467
+ var FALLBACK = "Technical explanations in English, keep command output as-is";
468
+ function detectSystemLanguage() {
469
+ try {
470
+ const locale = Intl.DateTimeFormat().resolvedOptions().locale;
471
+ return locale.split("-")[0].toLowerCase();
472
+ } catch {
473
+ return "en";
474
+ }
475
+ }
476
+ function languageInstruction(lang) {
477
+ const code = lang || detectSystemLanguage();
478
+ return LANGUAGE_MAP[code] ?? FALLBACK;
479
+ }
480
+
481
+ // src/i18n/en.ts
482
+ var en = {
483
+ // User preferences defaults
484
+ "prefs.comment_style": "follow existing code comment style",
485
+ "prefs.sudo": "list commands for user to run manually",
486
+ // Decision log
487
+ "decision.title": "# Decision Log \u2014 {date}",
488
+ "decision.affected_file": "**Affected file**: {path}",
489
+ "decision.name_change": "**Name change**: `{old}` \u2192 `{new}`",
490
+ "decision.stats_title": "## Change Statistics",
491
+ "decision.lines_added": "- Lines added: {count}",
492
+ "decision.lines_removed": "- Lines removed: {count}",
493
+ "decision.structure_title": "## Structure Changes",
494
+ "decision.section_removed": "**Removed sections**: {sections}",
495
+ "decision.section_added": "**Added sections**: {sections}",
496
+ "decision.reason_title": "## Reason for Change",
497
+ "decision.reason_placeholder": "_(Please document the reason for this change here)_",
498
+ // Migration map
499
+ "migration.entry": "- `{old}/` is replaced by `{new}`"
500
+ };
501
+ var en_default = en;
502
+
503
+ // src/i18n/zh.ts
504
+ var zh = {
505
+ // 用户偏好默认值
506
+ "prefs.comment_style": "\u4FDD\u6301\u73B0\u6709\u4EE3\u7801\u6CE8\u91CA\u98CE\u683C",
507
+ "prefs.sudo": "\u5217\u51FA\u547D\u4EE4\u8BA9\u7528\u6237\u624B\u52A8\u6267\u884C",
508
+ // 决策日志
509
+ "decision.title": "# \u51B3\u7B56\u65E5\u5FD7 \u2014 {date}",
510
+ "decision.affected_file": "**\u5F71\u54CD\u6587\u4EF6**: {path}",
511
+ "decision.name_change": "**\u540D\u79F0\u53D8\u66F4**: `{old}` \u2192 `{new}`",
512
+ "decision.stats_title": "## \u53D8\u66F4\u7EDF\u8BA1",
513
+ "decision.lines_added": "- \u65B0\u589E\u884C: {count}",
514
+ "decision.lines_removed": "- \u5220\u9664\u884C: {count}",
515
+ "decision.structure_title": "## \u7ED3\u6784\u53D8\u66F4",
516
+ "decision.section_removed": "**\u79FB\u9664\u7AE0\u8282**: {sections}",
517
+ "decision.section_added": "**\u65B0\u589E\u7AE0\u8282**: {sections}",
518
+ "decision.reason_title": "## \u53D8\u66F4\u539F\u56E0",
519
+ "decision.reason_placeholder": "_\uFF08\u8BF7\u5728\u6B64\u8865\u5145\u53D8\u66F4\u539F\u56E0\uFF09_",
520
+ // 迁移映射
521
+ "migration.entry": "- `{old}/` \u2192 \u5DF2\u6709 `{new}`"
522
+ };
523
+ var zh_default = zh;
524
+
525
+ // src/i18n/index.ts
526
+ var LOCALES = { en: en_default, zh: zh_default };
527
+ var DEFAULT_LOCALE = "en";
528
+ function detectLocale() {
529
+ try {
530
+ const lang = Intl.DateTimeFormat().resolvedOptions().locale.split("-")[0].toLowerCase();
531
+ if (LOCALES[lang]) return lang;
532
+ } catch {
533
+ }
534
+ return DEFAULT_LOCALE;
535
+ }
536
+ var activeLocale = detectLocale();
537
+ function t(key, params) {
538
+ const locale = LOCALES[activeLocale] ?? LOCALES[DEFAULT_LOCALE];
539
+ let msg = locale[key] ?? LOCALES[DEFAULT_LOCALE][key] ?? key;
540
+ if (params) {
541
+ for (const [k, v] of Object.entries(params)) {
542
+ msg = msg.replace(`{${k}}`, String(v));
543
+ }
544
+ }
545
+ return msg;
546
+ }
547
+
548
+ // src/layers/user-layer.ts
549
+ function defaultPreferences() {
550
+ return {
551
+ user: process.env.USER || process.env.USERNAME || "unknown",
552
+ last_updated: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
553
+ output_preferences: {
554
+ comment_style: t("prefs.comment_style"),
555
+ language: languageInstruction(detectSystemLanguage()),
556
+ verbosity: "concise"
557
+ },
558
+ security_boundaries: {
559
+ sudo: t("prefs.sudo")
560
+ }
561
+ };
562
+ }
563
+ function userProfilePath(config) {
564
+ const storage = config.resolvedStorages.user;
565
+ if (storage.type !== "local") return "";
566
+ const user = process.env.USER || process.env.USERNAME || "unknown";
567
+ return join4(storage.basePath, storage.template.replace("{name}", user));
568
+ }
569
+ function loadUserPreferences(config) {
570
+ const defaults = defaultPreferences();
571
+ const path = userProfilePath(config);
572
+ if (!path) return defaults;
573
+ try {
574
+ const raw = readFileSync5(path, "utf-8");
575
+ return { ...defaults, ...JSON.parse(raw) };
576
+ } catch {
577
+ return defaults;
578
+ }
579
+ }
580
+
581
+ // src/detection/toolchain.ts
582
+ import { exec } from "child_process";
583
+ import { promisify } from "util";
584
+ import { platform as platform3 } from "os";
585
+ var execAsync = promisify(exec);
586
+ var TOOL_DETECTORS = {
587
+ java: {
588
+ binary: "java",
589
+ versionFlag: "-version 2>&1",
590
+ versionRegex: /version\s+"?([\d._]+)"?/,
591
+ extraPath: ["javac", "mvn"]
592
+ },
593
+ go: {
594
+ binary: "go",
595
+ versionFlag: "version",
596
+ versionRegex: /go(\d+\.\d+\.\d+)/,
597
+ extraPath: ["gofmt"]
598
+ },
599
+ node: {
600
+ binary: "node",
601
+ versionFlag: "--version",
602
+ versionRegex: /v(\d+\.\d+\.\d+)/,
603
+ extraPath: ["npm", "npx"]
604
+ },
605
+ python: {
606
+ binary: "python3",
607
+ versionFlag: "--version 2>&1",
608
+ versionRegex: /Python (\d+\.\d+\.\d+)/,
609
+ extraPath: ["pip3"]
610
+ }
611
+ };
612
+ async function detectCommand(cmd) {
613
+ try {
614
+ const isWin = platform3() === "win32";
615
+ const shellCmd = isWin ? `where ${cmd} 2>nul` : `command -v ${cmd}`;
616
+ const { stdout } = await execAsync(shellCmd, { timeout: 5e3 });
617
+ return stdout.trim().split("\n")[0] || null;
618
+ } catch {
619
+ return null;
620
+ }
621
+ }
622
+ async function detectVersion(cmd, flag) {
623
+ try {
624
+ const { stdout, stderr } = await execAsync(`${cmd} ${flag}`, { timeout: 1e4 });
625
+ const output = stdout + stderr;
626
+ return output.trim() || null;
627
+ } catch {
628
+ return null;
629
+ }
630
+ }
631
+ async function detectToolchain() {
632
+ const result = {
633
+ java: null,
634
+ go: null,
635
+ node: null,
636
+ python: null
637
+ };
638
+ for (const [domain, detector] of Object.entries(TOOL_DETECTORS)) {
639
+ const binaryPath = await detectCommand(detector.binary);
640
+ if (!binaryPath) continue;
641
+ const versionOutput = await detectVersion(detector.binary, detector.versionFlag);
642
+ const versionMatch = versionOutput?.match(detector.versionRegex);
643
+ const version = versionMatch?.[1] || "unknown";
644
+ const paths = {};
645
+ const versions = {};
646
+ paths[detector.binary] = binaryPath;
647
+ versions[detector.binary] = version;
648
+ if (detector.extraPath) {
649
+ for (const extra of detector.extraPath) {
650
+ const p = await detectCommand(extra);
651
+ if (p) paths[extra] = p;
652
+ const v = await detectVersion(extra, "--version 2>&1");
653
+ if (v) versions[extra] = v.trim();
654
+ }
655
+ }
656
+ result[domain] = { paths, versions };
657
+ }
658
+ return result;
659
+ }
660
+
661
+ // src/hooks/session-created.ts
662
+ import { readFileSync as readFileSync7 } from "fs";
663
+ import { execSync as execSync2 } from "child_process";
664
+ import { join as join6 } from "path";
665
+
666
+ // src/audit/migration-map.ts
667
+ import { readFileSync as readFileSync6, existsSync as existsSync3, readdirSync } from "fs";
668
+ import { join as join5 } from "path";
669
+ function parseMigrationMap(content, sourceFile) {
670
+ const entries = [];
671
+ const mapSection = content.match(/### Migration Map\n([\s\S]*?)(?=\n###|\n##|\n#|$)/);
672
+ if (!mapSection) return entries;
673
+ const lines = mapSection[1].split("\n");
674
+ for (const line of lines) {
675
+ const match = line.match(
676
+ /-\s*`([^`]+)`\s*(?:→|is replaced by)\s*(?:已有\s*)?`?([^`\n]+?)`?\s*$/
677
+ );
678
+ if (match) {
679
+ entries.push({
680
+ oldPath: match[1].trim(),
681
+ newApi: match[2].trim(),
682
+ sourceFile
683
+ });
684
+ }
685
+ }
686
+ return entries;
687
+ }
688
+ function findSkillFiles(skillsDir) {
689
+ const results = [];
690
+ function walk(dir) {
691
+ if (!existsSync3(dir)) return;
692
+ try {
693
+ const entries = readdirSync(dir, { withFileTypes: true });
694
+ for (const entry of entries) {
695
+ const fullPath = join5(dir, entry.name);
696
+ if (entry.isDirectory()) {
697
+ if (!entry.name.startsWith("_archive") && !entry.name.startsWith("decisions")) {
698
+ walk(fullPath);
699
+ }
700
+ } else if (entry.name.endsWith("SKILL.md") || entry.name.endsWith("skill.md")) {
701
+ results.push(fullPath);
702
+ }
703
+ }
704
+ } catch {
705
+ }
706
+ }
707
+ walk(skillsDir);
708
+ return results;
709
+ }
710
+ function parseAllMigrationMaps(skillsDir) {
711
+ const results = [];
712
+ const files = findSkillFiles(skillsDir);
713
+ for (const file of files) {
714
+ try {
715
+ const raw = readFileSync6(file, "utf-8");
716
+ const entries = parseMigrationMap(raw, file);
717
+ results.push(...entries);
718
+ } catch {
719
+ }
720
+ }
721
+ return results;
722
+ }
723
+ function formatMigrationHints(entries) {
724
+ if (entries.length === 0) return "";
725
+ const lines = [];
726
+ lines.push("## Available Migrations");
727
+ lines.push("");
728
+ lines.push("When modifying these files, consider upgrading:");
729
+ lines.push("");
730
+ for (const entry of entries) {
731
+ lines.push(`- \`${entry.oldPath}\` is replaced by \`${entry.newApi}\``);
732
+ }
733
+ lines.push("");
734
+ return lines.join("\n");
735
+ }
736
+
737
+ // src/hooks/session-created.ts
738
+ function getCurrentBranch(projectRoot) {
739
+ try {
740
+ return execSync2("git branch --show-current", {
741
+ cwd: projectRoot,
742
+ encoding: "utf-8",
743
+ timeout: 3e3
744
+ }).trim();
745
+ } catch {
746
+ return "unknown";
747
+ }
748
+ }
749
+ function getProjectName(config) {
750
+ try {
751
+ const pkg = JSON.parse(
752
+ readFileSync7(join6(config.projectRoot, "package.json"), "utf-8")
753
+ );
754
+ return pkg.name || config.projectRoot.split("/").pop() || "unknown";
755
+ } catch {
756
+ return config.projectRoot.split("/").pop() || "unknown";
757
+ }
758
+ }
759
+ function domainNames(domains) {
760
+ return Object.keys(domains).join(", ") || "none";
761
+ }
762
+ function buildKnowledgeRoutingRules(config) {
763
+ return config.behavior_rules.filter((r) => r.enabled).map((r) => `- ${r.rule}`).join("\n");
764
+ }
765
+ function createSessionCreatedHandler(client, config, _registry) {
766
+ return async (input, _output) => {
767
+ const sessionId = input.sessionId || input.path?.id;
768
+ if (!sessionId) return;
769
+ const platform4 = getPlatformContext(config);
770
+ const toolchain = await detectToolchain();
771
+ const domains = {};
772
+ for (const [name, t2] of Object.entries(toolchain)) {
773
+ if (t2) {
774
+ domains[`${name}_dev`] = { paths: t2.paths, versions: t2.versions };
775
+ }
776
+ }
777
+ let machine = loadMachineProfile(config);
778
+ if (!machine) {
779
+ machine = createMachineProfile(config, domains);
780
+ saveMachineProfile(config, machine);
781
+ } else if (Object.keys(domains).length > 0) {
782
+ machine.domains = { ...domains, ...machine.domains };
783
+ saveMachineProfile(config, machine);
784
+ }
785
+ const runtime = getProjectRuntime(config);
786
+ const recorded = getRecordedRequirements(config);
787
+ const versionWarnings = diffRuntimeVersions(runtime, recorded);
788
+ const branch = getCurrentBranch(config.projectRoot);
789
+ const userPrefs = loadUserPreferences(config);
790
+ const projectName = getProjectName(config);
791
+ const skillsDir = config.resolvedStorages.project?.basePath;
792
+ const migrationEntries = skillsDir ? parseAllMigrationMaps(skillsDir) : [];
793
+ const contextParts = [];
794
+ contextParts.push("## Session Context");
795
+ contextParts.push("");
796
+ contextParts.push(formatPlatformContext(platform4));
797
+ contextParts.push(`Your machine: ${machine.machine_id} (${machine.hostname})`);
798
+ contextParts.push(`Available toolchains: ${domainNames(machine.domains)}`);
799
+ contextParts.push(
800
+ "Call get_machine_context(domain) for detailed toolchain info"
801
+ );
802
+ contextParts.push(
803
+ "To switch tool versions, add a `switching` command in .opencode/skills/_shared/runtime-requirements.yaml"
804
+ );
805
+ contextParts.push("");
806
+ contextParts.push("## User Preferences");
807
+ contextParts.push("");
808
+ const prefs = userPrefs.output_preferences;
809
+ contextParts.push(`Comment style: ${prefs.comment_style}`);
810
+ contextParts.push(`Language: ${prefs.language}`);
811
+ contextParts.push("(Replies automatically adapt to the user's language)");
812
+ contextParts.push(`Verbosity: ${prefs.verbosity}`);
813
+ contextParts.push(`Security boundary: ${userPrefs.security_boundaries.sudo}`);
814
+ contextParts.push("");
815
+ if (runtime.languages.length > 0) {
816
+ contextParts.push("## Project Info");
817
+ contextParts.push("");
818
+ contextParts.push(`Current project: ${projectName}`);
819
+ contextParts.push(`Languages: ${runtime.languages.join(", ")}`);
820
+ if (Object.keys(runtime.versions).length > 0) {
821
+ contextParts.push(
822
+ `Runtime versions: ${Object.entries(runtime.versions).map(([k, v]) => `${k}=${v}`).join(", ")}`
823
+ );
824
+ }
825
+ if (runtime.build) contextParts.push(`Build command: ${runtime.build}`);
826
+ if (runtime.test) contextParts.push(`Test command: ${runtime.test}`);
827
+ if (branch !== "unknown") {
828
+ contextParts.push(`Current branch: ${branch}`);
829
+ }
830
+ contextParts.push("");
831
+ }
832
+ contextParts.push("## Knowledge Routing Rules");
833
+ contextParts.push("");
834
+ contextParts.push("When discovering knowledge worth recording, follow these rules:");
835
+ contextParts.push(buildKnowledgeRoutingRules(config));
836
+ contextParts.push("When uncertain \u2192 write to SKILL.md and ask the user");
837
+ contextParts.push("");
838
+ if (migrationEntries.length > 0) {
839
+ contextParts.push(formatMigrationHints(migrationEntries));
840
+ }
841
+ if (versionWarnings.length > 0) {
842
+ contextParts.push("## Version Mismatch Detected");
843
+ contextParts.push("");
844
+ for (const w of versionWarnings) {
845
+ contextParts.push(`- ${w}`);
846
+ }
847
+ contextParts.push("If the upgrade is intentional, update runtime-requirements.yaml");
848
+ contextParts.push("");
849
+ }
850
+ const compiledContext = contextParts.join("\n");
851
+ try {
852
+ await client.session.prompt({
853
+ path: { id: sessionId },
854
+ body: {
855
+ noReply: true,
856
+ parts: [{ type: "text", text: compiledContext }]
857
+ }
858
+ });
859
+ } catch {
860
+ }
861
+ };
862
+ }
863
+
864
+ // src/hooks/file-edited.ts
865
+ import { readFileSync as readFileSync9 } from "fs";
866
+ import { execSync as execSync3 } from "child_process";
867
+ import { relative } from "path";
868
+
869
+ // src/audit/change-detector.ts
870
+ function extractFrontmatterName(content) {
871
+ const match = content.match(/^---\s*\nname:\s*(\S+)/m);
872
+ return match?.[1]?.trim() || null;
873
+ }
874
+ function extractHeadings(content) {
875
+ const matches = content.match(/^##\s+(.+)$/gm);
876
+ return matches ? matches.map((m) => m.replace(/^##\s+/, "").trim()) : [];
877
+ }
878
+ function lineDiff(oldContent, newContent) {
879
+ const oldLines = oldContent.split("\n").length;
880
+ const newLines = newContent.split("\n").length;
881
+ const diff = newLines - oldLines;
882
+ return {
883
+ added: diff > 0 ? diff : 0,
884
+ removed: diff < 0 ? -diff : 0
885
+ };
886
+ }
887
+ function analyzeChange(oldContent, newContent, filePath) {
888
+ if (!oldContent) {
889
+ return {
890
+ isSubstantial: false,
891
+ oldName: null,
892
+ newName: extractFrontmatterName(newContent),
893
+ oldHeadings: [],
894
+ newHeadings: extractHeadings(newContent),
895
+ linesAdded: 0,
896
+ linesRemoved: 0
897
+ };
898
+ }
899
+ const oldName = extractFrontmatterName(oldContent);
900
+ const newName = extractFrontmatterName(newContent);
901
+ const nameChanged = oldName !== null && newName !== null && oldName !== newName;
902
+ const diff = lineDiff(oldContent, newContent);
903
+ const totalLines = Math.max(oldContent.split("\n").length, 1);
904
+ const diffRatio = (diff.added + diff.removed) / totalLines;
905
+ const isSubstantial = nameChanged || diff.added + diff.removed > 5 || diffRatio > 0.2;
906
+ return {
907
+ isSubstantial,
908
+ oldName,
909
+ newName,
910
+ oldHeadings: extractHeadings(oldContent),
911
+ newHeadings: extractHeadings(newContent),
912
+ linesAdded: diff.added,
913
+ linesRemoved: diff.removed
914
+ };
915
+ }
916
+
917
+ // src/audit/archive-manager.ts
918
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4, renameSync } from "fs";
919
+ import { join as join7, dirname as dirname2 } from "path";
920
+ function moduleDir(filePath) {
921
+ const segments = filePath.split("/");
922
+ const skillsIdx = segments.lastIndexOf(".opencode");
923
+ if (skillsIdx === -1) return dirname2(filePath);
924
+ const skillsDir = join7(segments.slice(0, skillsIdx + 2).join("/"));
925
+ const relative2 = filePath.replace(skillsDir + "/", "");
926
+ const parts = relative2.split("/");
927
+ return parts.length > 1 ? join7(skillsDir, parts[0]) : skillsDir;
928
+ }
929
+ function createDecisionLog(filePath, change) {
930
+ const dir = moduleDir(filePath);
931
+ const decisionsDir = join7(dir, "decisions");
932
+ mkdirSync2(decisionsDir, { recursive: true });
933
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
934
+ const topic = change.oldName || change.newName || "skill";
935
+ const logPath = join7(decisionsDir, `${date}-${topic}.md`);
936
+ const lines = [];
937
+ lines.push(t("decision.title", { date }));
938
+ lines.push("");
939
+ lines.push(t("decision.affected_file", { path: filePath }));
940
+ lines.push("");
941
+ if (change.oldName && change.newName && change.oldName !== change.newName) {
942
+ lines.push(t("decision.name_change", { old: change.oldName, new: change.newName }));
943
+ lines.push("");
944
+ }
945
+ lines.push(t("decision.stats_title"));
946
+ lines.push("");
947
+ lines.push(t("decision.lines_added", { count: change.linesAdded }));
948
+ lines.push(t("decision.lines_removed", { count: change.linesRemoved }));
949
+ lines.push("");
950
+ if (change.oldHeadings.length > 0 || change.newHeadings.length > 0) {
951
+ lines.push(t("decision.structure_title"));
952
+ lines.push("");
953
+ const removed = change.oldHeadings.filter((h) => !change.newHeadings.includes(h));
954
+ const added = change.newHeadings.filter((h) => !change.oldHeadings.includes(h));
955
+ if (removed.length > 0) {
956
+ lines.push(t("decision.section_removed", { sections: removed.join(", ") }));
957
+ }
958
+ if (added.length > 0) {
959
+ lines.push(t("decision.section_added", { sections: added.join(", ") }));
960
+ }
961
+ lines.push("");
962
+ }
963
+ lines.push(t("decision.reason_title"));
964
+ lines.push("");
965
+ lines.push(t("decision.reason_placeholder"));
966
+ lines.push("");
967
+ writeFileSync2(logPath, lines.join("\n"), "utf-8");
968
+ return logPath;
969
+ }
970
+ function archiveOldVariant(filePath, change) {
971
+ if (!change.oldName || !change.newName || change.oldName === change.newName) {
972
+ return null;
973
+ }
974
+ const dir = moduleDir(filePath);
975
+ const archiveDir = join7(dir, "_archive", change.oldName);
976
+ mkdirSync2(archiveDir, { recursive: true });
977
+ const archivePath = join7(archiveDir, "SKILL.md");
978
+ if (existsSync4(filePath)) {
979
+ try {
980
+ renameSync(filePath, archivePath);
981
+ } catch {
982
+ const content = readFileSync8(filePath, "utf-8");
983
+ writeFileSync2(archivePath, content, "utf-8");
984
+ }
985
+ }
986
+ return archivePath;
987
+ }
988
+ function updateMigrationMap(filePath, oldName, newName) {
989
+ try {
990
+ const content = readFileSync8(filePath, "utf-8");
991
+ const lines = content.split("\n");
992
+ let migrationIdx = lines.findIndex((l) => l.startsWith("### Migration Map"));
993
+ const entry = t("migration.entry", { old: oldName, new: newName });
994
+ if (migrationIdx === -1) {
995
+ lines.push("");
996
+ lines.push("### Migration Map");
997
+ lines.push(entry);
998
+ } else {
999
+ let insertIdx = migrationIdx + 2;
1000
+ while (insertIdx < lines.length && (lines[insertIdx].trim() === "" || lines[insertIdx].startsWith("-"))) {
1001
+ insertIdx++;
1002
+ }
1003
+ lines.splice(insertIdx, 0, entry);
1004
+ }
1005
+ writeFileSync2(filePath, lines.join("\n"), "utf-8");
1006
+ } catch {
1007
+ }
1008
+ }
1009
+
1010
+ // src/hooks/file-edited.ts
1011
+ function getOldContent(filePath, projectRoot) {
1012
+ try {
1013
+ const relPath = relative(projectRoot, filePath);
1014
+ return execSync3(`git show HEAD:"${relPath}"`, {
1015
+ cwd: projectRoot,
1016
+ encoding: "utf-8",
1017
+ timeout: 3e3
1018
+ }).toString();
1019
+ } catch {
1020
+ return null;
1021
+ }
1022
+ }
1023
+ function createFileEditedHandler(config, _registry) {
1024
+ return async (input, _output) => {
1025
+ const filePath = input.filePath || input.path || "";
1026
+ if (!filePath) return;
1027
+ const skillsDir = config.resolvedStorages.project?.basePath;
1028
+ if (!skillsDir || !filePath.startsWith(skillsDir)) return;
1029
+ const isSkillFile = filePath.endsWith("SKILL.md") || filePath.endsWith("skill.md");
1030
+ if (!isSkillFile) return;
1031
+ let newContent;
1032
+ try {
1033
+ newContent = readFileSync9(filePath, "utf-8");
1034
+ } catch {
1035
+ return;
1036
+ }
1037
+ const oldContent = getOldContent(filePath, config.projectRoot);
1038
+ const change = analyzeChange(oldContent, newContent, filePath);
1039
+ if (!change.isSubstantial) return;
1040
+ createDecisionLog(filePath, change);
1041
+ if (change.oldName && change.newName && change.oldName !== change.newName) {
1042
+ archiveOldVariant(filePath, change);
1043
+ updateMigrationMap(filePath, change.oldName, change.newName);
1044
+ }
1045
+ };
1046
+ }
1047
+
1048
+ // src/security/sanitizer.ts
1049
+ var SENSITIVE_CONTENT_PATTERNS = [
1050
+ /-----BEGIN\s+(RSA|DSA|EC|OPENSSH)?\s*PRIVATE\s+KEY-----/,
1051
+ /export\s+\w*TOKEN\w*\s*=\s*\S+/i,
1052
+ /export\s+\w*SECRET\w*\s*=\s*\S+/i,
1053
+ /export\s+\w*KEY\w*\s*=\s*\S+/i,
1054
+ /export\s+\w*PASSWORD\w*\s*=\s*\S+/i,
1055
+ /[\w-]{20,}={2,}$/
1056
+ ];
1057
+ function containsSensitiveContent(content) {
1058
+ const found = [];
1059
+ for (const pattern of SENSITIVE_CONTENT_PATTERNS) {
1060
+ if (pattern.test(content)) {
1061
+ found.push(pattern.source);
1062
+ }
1063
+ }
1064
+ return found;
1065
+ }
1066
+ function shouldBlockRead(filePath) {
1067
+ if (filePath.includes(".env")) return true;
1068
+ const sshMatch = /\/\.ssh\//;
1069
+ if (sshMatch.test(filePath)) return true;
1070
+ const gnupgMatch = /\/\.gnupg\//;
1071
+ if (gnupgMatch.test(filePath)) return true;
1072
+ return false;
1073
+ }
1074
+
1075
+ // src/hooks/tool-execute-before.ts
1076
+ var BUILD_RUN_PATTERNS = {
1077
+ java: ["mvn ", "gradle ", "gradlew ", "java ", "javac "],
1078
+ go: ["go build", "go test", "go run", "go install", "go generate"],
1079
+ node: ["npm ", "npx ", "node ", "yarn ", "pnpm "],
1080
+ python: ["python ", "python3 ", "pip ", "pip3 ", "pytest"],
1081
+ rust: ["cargo "],
1082
+ generic: ["make ", "cmake "]
1083
+ };
1084
+ function isBuildOrRunCommand(command) {
1085
+ for (const patterns of Object.values(BUILD_RUN_PATTERNS)) {
1086
+ for (const pattern of patterns) {
1087
+ if (command.startsWith(pattern)) return true;
1088
+ }
1089
+ }
1090
+ return false;
1091
+ }
1092
+ function detectLanguage(command) {
1093
+ for (const [lang, patterns] of Object.entries(BUILD_RUN_PATTERNS)) {
1094
+ for (const pattern of patterns) {
1095
+ if (command.startsWith(pattern)) return lang;
1096
+ }
1097
+ }
1098
+ return null;
1099
+ }
1100
+ function buildJavaSwitching(toolchain, requiredVersion) {
1101
+ const java = toolchain.java;
1102
+ if (!java?.paths?.java) return "";
1103
+ const javaPath = java.paths.java;
1104
+ const javaHome = javaPath.replace(/\/bin\/java$/, "");
1105
+ if (requiredVersion && java.versions?.java) {
1106
+ if (!java.versions.java.includes(requiredVersion.replace(/^(\d+).*/, "$1"))) {
1107
+ const lower = requiredVersion.replace(/^(\d+).*/, "$1");
1108
+ const altPath = javaPath.replace(/java-?\d+/, `java-${lower}`);
1109
+ return `export JAVA_HOME="${altPath.replace(/\/bin\/java$/, "")}" && export PATH="$JAVA_HOME/bin:$PATH" && `;
1110
+ }
1111
+ }
1112
+ return `export JAVA_HOME="${javaHome}" && export PATH="$JAVA_HOME/bin:$PATH" && `;
1113
+ }
1114
+ function buildGoSwitching(toolchain) {
1115
+ const go = toolchain.go;
1116
+ if (!go?.paths?.go) return "";
1117
+ return `export PATH="${go.paths.go.replace(/\/go$/, "")}:$PATH" && `;
1118
+ }
1119
+ function getRuntimeSwitchingCmd(recordedRequirements, lang) {
1120
+ if (!recordedRequirements) return null;
1121
+ return recordedRequirements[lang]?.switching || null;
1122
+ }
1123
+ function createToolExecuteBeforeHandler(config, _registry) {
1124
+ return async (input, output) => {
1125
+ if (input.tool === "read" && output.args?.filePath) {
1126
+ if (shouldBlockRead(output.args.filePath)) {
1127
+ throw new Error("Cannot read sensitive files (.env, .ssh/, .gnupg/)");
1128
+ }
1129
+ }
1130
+ if (input.tool !== "bash" || !output.args?.command) return;
1131
+ const command = output.args.command;
1132
+ if (!isBuildOrRunCommand(command)) return;
1133
+ const lang = detectLanguage(command);
1134
+ if (!lang || lang === "generic") return;
1135
+ const recorded = getRecordedRequirements(config);
1136
+ let switchingCmd = getRuntimeSwitchingCmd(recorded, lang);
1137
+ if (switchingCmd) {
1138
+ output.args.command = `${switchingCmd} && ${output.args.command}`;
1139
+ return;
1140
+ }
1141
+ const machine = loadMachineProfile(config);
1142
+ if (machine?.domains) {
1143
+ const machineDomain = machine.domains[`${lang}_dev`];
1144
+ if (machineDomain?.versions) {
1145
+ const required = recorded?.[lang]?.version;
1146
+ for (const [binary, version] of Object.entries(machineDomain.versions)) {
1147
+ if (required && !version.includes(required.replace(/[+>=<~^]/, ""))) {
1148
+ continue;
1149
+ }
1150
+ const binaryPath = machineDomain.paths?.[binary];
1151
+ if (binaryPath && lang === "java") {
1152
+ const javaHome = binaryPath.replace(/\/bin\/java$/, "");
1153
+ output.args.command = `export JAVA_HOME="${javaHome}" && export PATH="$JAVA_HOME/bin:$PATH" && ${output.args.command}`;
1154
+ return;
1155
+ }
1156
+ }
1157
+ }
1158
+ }
1159
+ const toolchain = await detectToolchain();
1160
+ switch (lang) {
1161
+ case "java":
1162
+ switchingCmd = buildJavaSwitching(toolchain, recorded?.["java"]?.version);
1163
+ break;
1164
+ case "go":
1165
+ switchingCmd = buildGoSwitching(toolchain);
1166
+ break;
1167
+ }
1168
+ if (switchingCmd) {
1169
+ output.args.command = `${switchingCmd}${output.args.command}`;
1170
+ }
1171
+ };
1172
+ }
1173
+
1174
+ // src/tools/note-discovery.ts
1175
+ import { tool } from "@opencode-ai/plugin";
1176
+
1177
+ // src/utils/parsers.ts
1178
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
1179
+ import { join as join8, dirname as dirname3 } from "path";
1180
+ var parserRegistry = /* @__PURE__ */ new Map();
1181
+ function registerParser(ext, parser) {
1182
+ parserRegistry.set(ext, parser);
1183
+ }
1184
+ function readParsed(basePath, relPath) {
1185
+ const fullPath = join8(basePath, relPath);
1186
+ try {
1187
+ if (!existsSync6(fullPath)) return null;
1188
+ const raw = readFileSync10(fullPath, "utf-8");
1189
+ const ext = relPath.split(".").pop() || "";
1190
+ const parser = parserRegistry.get(ext);
1191
+ return { raw, parsed: parser ? parser(raw) : null };
1192
+ } catch {
1193
+ return null;
1194
+ }
1195
+ }
1196
+ function defaultYamlParser(content) {
1197
+ const result = {};
1198
+ const lines = content.split("\n");
1199
+ let currentKey = "";
1200
+ for (const line of lines) {
1201
+ const match = line.match(/^(\w[\w.-]*):\s*(.*)/);
1202
+ if (match) {
1203
+ currentKey = match[1];
1204
+ result[currentKey] = match[2] || "";
1205
+ } else if (currentKey) {
1206
+ result[currentKey] = result[currentKey] + "\n" + line;
1207
+ }
1208
+ }
1209
+ return result;
1210
+ }
1211
+ registerParser("yaml", defaultYamlParser);
1212
+ registerParser("yml", defaultYamlParser);
1213
+ registerParser("json", (c) => JSON.parse(c));
1214
+ registerParser("md", (c) => ({ raw: c }));
1215
+
1216
+ // src/utils/knowledge-router.ts
1217
+ import { existsSync as existsSync7 } from "fs";
1218
+ import { join as join9 } from "path";
1219
+ import { execSync as execSync4 } from "child_process";
1220
+ function isGitRepo(projectRoot) {
1221
+ try {
1222
+ const output = execSync4("git rev-parse --is-inside-work-tree", {
1223
+ cwd: projectRoot,
1224
+ encoding: "utf-8",
1225
+ timeout: 3e3
1226
+ });
1227
+ return output.trim() === "true";
1228
+ } catch {
1229
+ return existsSync7(join9(projectRoot, ".git"));
1230
+ }
1231
+ }
1232
+ function isCoreArchitecture(content) {
1233
+ const keywords = [
1234
+ "architecture",
1235
+ "\u8BBE\u8BA1\u51B3\u7B56",
1236
+ "design decision",
1237
+ "interface contract",
1238
+ "\u63A5\u53E3\u7EA6\u5B9A",
1239
+ "\u63A5\u53E3",
1240
+ "interface",
1241
+ "protocol",
1242
+ "\u6743\u9650\u6A21\u578B",
1243
+ "permission model",
1244
+ "token",
1245
+ "auth",
1246
+ "authentication",
1247
+ "\u8BA4\u8BC1",
1248
+ "authorization",
1249
+ "api contract"
1250
+ ];
1251
+ return keywords.some((kw) => content.toLowerCase().includes(kw.toLowerCase()));
1252
+ }
1253
+ function determineScope(note, config) {
1254
+ if (note.scope === "cross") return "cross";
1255
+ if (note.layer === "project") {
1256
+ if (isCoreArchitecture(note.content) && isGitRepo(config.projectRoot)) {
1257
+ return "cross";
1258
+ }
1259
+ }
1260
+ return "branch";
1261
+ }
1262
+
1263
+ // src/tools/note-discovery.ts
1264
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, existsSync as existsSync8 } from "fs";
1265
+ import { join as join10, dirname as dirname4 } from "path";
1266
+ function createNoteDiscoveryTool(config) {
1267
+ return tool({
1268
+ description: "Record discovered knowledge. Core architecture/design decisions \u2192 _shared/ (cross-branch). Branch progress \u2192 SKILL.md. Only tool outputs go to Machine/Platform layers; user assumptions never enter any layer.",
1269
+ args: {
1270
+ domain: tool.schema.string().describe("Domain the knowledge belongs to"),
1271
+ content: tool.schema.string().describe("Knowledge content to record"),
1272
+ layer: tool.schema.enum(["project", "machine", "user"]).describe("Target layer"),
1273
+ scope: tool.schema.enum(["branch", "cross"]).optional().describe("Scope; auto-determined if not specified")
1274
+ },
1275
+ async execute(args, _context) {
1276
+ const note = {
1277
+ domain: args.domain,
1278
+ content: args.content,
1279
+ layer: args.layer,
1280
+ scope: args.scope
1281
+ };
1282
+ const sensitive = containsSensitiveContent(note.content);
1283
+ if (sensitive.length > 0) {
1284
+ return "Content contains sensitive data (private keys, tokens, secrets) and will not be recorded";
1285
+ }
1286
+ const scope = determineScope(note, config);
1287
+ if (note.layer === "project") {
1288
+ const storage = config.resolvedStorages.project;
1289
+ if (storage.type !== "local") {
1290
+ return `Project layer storage not configured; cannot record "${note.domain}"`;
1291
+ }
1292
+ let targetPath;
1293
+ if (scope === "cross") {
1294
+ targetPath = join10(storage.basePath, "_shared", `${note.domain}.md`);
1295
+ } else {
1296
+ targetPath = join10(storage.basePath, `${note.domain}.md`);
1297
+ }
1298
+ mkdirSync4(dirname4(targetPath), { recursive: true });
1299
+ const existing = existsSync8(targetPath) ? readParsed(storage.basePath, targetPath)?.raw || "" : "";
1300
+ const marker = scope === "cross" ? "## Cross-branch Knowledge\n\n" : "## Branch Knowledge\n\n";
1301
+ const entry = existing ? `${existing}
1302
+
1303
+ ---
1304
+
1305
+ ${marker}${note.content}` : `# ${note.domain}
1306
+
1307
+ ${marker}${note.content}`;
1308
+ writeFileSync4(targetPath, entry, "utf-8");
1309
+ return scope === "cross" ? `Knowledge "${note.domain}" written to _shared/${note.domain}.md (cross-branch)` : `Knowledge "${note.domain}" written to SKILL.md (${note.domain}.md) (branch)`;
1310
+ }
1311
+ if (note.layer === "machine") {
1312
+ const storage = config.resolvedStorages.machine;
1313
+ if (storage.type !== "local") {
1314
+ return "Machine layer storage not configured; cannot record knowledge";
1315
+ }
1316
+ const machine = loadMachineProfile(config);
1317
+ if (!machine) {
1318
+ return "Machine profile does not exist; waiting for auto-initialization";
1319
+ }
1320
+ const domainKey = note.domain.endsWith("_dev") ? note.domain : `${note.domain}_dev`;
1321
+ if (!machine.domains[domainKey]) {
1322
+ machine.domains[domainKey] = {};
1323
+ }
1324
+ machine.domains[domainKey].versions ||= {};
1325
+ machine.domains[domainKey].versions[note.domain] = note.content;
1326
+ saveMachineProfile(config, machine);
1327
+ return `Machine layer ${domainKey} updated`;
1328
+ }
1329
+ return `Knowledge "${note.domain}" recorded to ${note.layer} layer (${scope})`;
1330
+ }
1331
+ });
1332
+ }
1333
+
1334
+ // src/tools/get-machine-context.ts
1335
+ import { tool as tool2 } from "@opencode-ai/plugin";
1336
+ function createGetMachineContextTool(config) {
1337
+ return tool2({
1338
+ description: "Get full details of a dev toolchain domain, including executable paths and versions",
1339
+ args: {
1340
+ domain: tool2.schema.string().describe("Toolchain domain, e.g. java_dev, go_dev, node_dev, python_dev")
1341
+ },
1342
+ async execute(args, _context) {
1343
+ const domain = args.domain;
1344
+ const machine = loadMachineProfile(config);
1345
+ if (machine?.domains?.[domain]) {
1346
+ const d = machine.domains[domain];
1347
+ const paths2 = d.paths ? Object.entries(d.paths).map(([k, v]) => ` ${k}: ${v}`).join("\n") : "";
1348
+ const versions2 = d.versions ? Object.entries(d.versions).map(([k, v]) => ` ${k}: ${v}`).join("\n") : "";
1349
+ return [
1350
+ `Machine domain: ${domain}`,
1351
+ paths2 ? `Executable paths:
1352
+ ${paths2}` : "",
1353
+ versions2 ? `Versions:
1354
+ ${versions2}` : "",
1355
+ d.nvm_dir ? `NVM: ${d.nvm_dir}` : "",
1356
+ d.gopath ? `GOPATH: ${d.gopath}` : ""
1357
+ ].filter(Boolean).join("\n");
1358
+ }
1359
+ const toolchain = await detectToolchain();
1360
+ const result = toolchain[domain?.replace("_dev", "")];
1361
+ if (!result) {
1362
+ return `No toolchain found for ${domain}; ensure the relevant dev tools are installed and on PATH`;
1363
+ }
1364
+ const paths = Object.entries(result.paths).map(([k, v]) => ` ${k}: ${v}`).join("\n");
1365
+ const versions = Object.entries(result.versions).map(([k, v]) => ` ${k}: ${v}`).join("\n");
1366
+ return [
1367
+ `Machine domain: ${domain} (live detection)`,
1368
+ `Executable paths:
1369
+ ${paths}`,
1370
+ `Versions:
1371
+ ${versions}`
1372
+ ].join("\n");
1373
+ }
1374
+ });
1375
+ }
1376
+
1377
+ // src/index.ts
1378
+ var plugin = (async (ctx) => {
1379
+ const config = resolvePluginConfig(ctx.directory);
1380
+ const registry = new LayerRegistry(config.layers);
1381
+ return {
1382
+ "session.created": createSessionCreatedHandler(ctx.client, config, registry),
1383
+ "file.edited": createFileEditedHandler(config, registry),
1384
+ "tool.execute.before": createToolExecuteBeforeHandler(config, registry),
1385
+ tool: {
1386
+ note_discovery: createNoteDiscoveryTool(config),
1387
+ get_machine_context: createGetMachineContextTool(config)
1388
+ }
1389
+ };
1390
+ });
1391
+ var index_default = plugin;
1392
+ export {
1393
+ index_default as default
1394
+ };