@tokentestai/ais 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.
@@ -0,0 +1,1400 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/automation/state.ts
4
+ import { chmodSync, existsSync } from "fs";
5
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
6
+ import { homedir } from "os";
7
+ import { dirname, join } from "path";
8
+
9
+ // src/automation/model.ts
10
+ var PROTECT_TOOLS = ["claude", "codex", "openclaw"];
11
+ var UPDATE_CHANNELS = ["latest", "next"];
12
+ function isProtectTool(value) {
13
+ return PROTECT_TOOLS.includes(value);
14
+ }
15
+ function isUpdateChannel(value) {
16
+ return UPDATE_CHANNELS.includes(value);
17
+ }
18
+
19
+ // src/automation/state.ts
20
+ var STATE_FILE_MODE = 384;
21
+ var DEFAULT_AUTOMATION_STATE_PATH_DISPLAY = "~/.ais/automation-state.json";
22
+ function createDefaultAutomationState() {
23
+ return {
24
+ protect: {
25
+ tools: Object.fromEntries(
26
+ PROTECT_TOOLS.map((tool) => [tool, createDefaultProtectToolRuntimeState()])
27
+ )
28
+ },
29
+ update: {
30
+ lastResult: "never",
31
+ skipNextCheck: false
32
+ }
33
+ };
34
+ }
35
+ function createDefaultProtectToolRuntimeState() {
36
+ return {
37
+ installed: false,
38
+ suspended: false
39
+ };
40
+ }
41
+ function resolveAutomationStatePath(path) {
42
+ return expandHomePath(path ?? DEFAULT_AUTOMATION_STATE_PATH_DISPLAY);
43
+ }
44
+ async function loadAutomationState(path) {
45
+ const resolvedPath = resolveAutomationStatePath(path);
46
+ if (!existsSync(resolvedPath)) {
47
+ return {
48
+ exists: false,
49
+ path: resolvedPath,
50
+ state: createDefaultAutomationState()
51
+ };
52
+ }
53
+ let parsed;
54
+ try {
55
+ parsed = JSON.parse(await readFile(resolvedPath, "utf8"));
56
+ } catch (error) {
57
+ return recoverAutomationState(resolvedPath, error);
58
+ }
59
+ try {
60
+ return {
61
+ exists: true,
62
+ path: resolvedPath,
63
+ state: mergeAutomationState(parsed)
64
+ };
65
+ } catch (error) {
66
+ return recoverAutomationState(resolvedPath, error);
67
+ }
68
+ }
69
+ async function saveAutomationState(state, path) {
70
+ const resolvedPath = resolveAutomationStatePath(path);
71
+ const payload = `${JSON.stringify(state, null, 2)}
72
+ `;
73
+ await mkdir(dirname(resolvedPath), { recursive: true });
74
+ await writeFile(resolvedPath, payload, { mode: STATE_FILE_MODE });
75
+ chmodSync(resolvedPath, STATE_FILE_MODE);
76
+ }
77
+ function cloneAutomationState(state) {
78
+ return {
79
+ protect: {
80
+ tools: Object.fromEntries(
81
+ PROTECT_TOOLS.map((tool) => [
82
+ tool,
83
+ {
84
+ ...state.protect.tools[tool]
85
+ }
86
+ ])
87
+ )
88
+ },
89
+ update: {
90
+ ...state.update
91
+ }
92
+ };
93
+ }
94
+ function mergeAutomationState(raw) {
95
+ if (!isRecord(raw)) {
96
+ throw new Error("Failed to load automation state: root must be an object");
97
+ }
98
+ const state = createDefaultAutomationState();
99
+ if ("update" in raw) {
100
+ const update = expectObject(raw.update, "update");
101
+ if ("lastResult" in update) {
102
+ state.update.lastResult = expectUpdateCheckResult(update.lastResult, "update.lastResult");
103
+ }
104
+ if ("skipNextCheck" in update) {
105
+ state.update.skipNextCheck = expectBoolean(update.skipNextCheck, "update.skipNextCheck");
106
+ }
107
+ if ("lastCheckedAt" in update) {
108
+ state.update.lastCheckedAt = expectOptionalFiniteNumber(update.lastCheckedAt, "update.lastCheckedAt");
109
+ }
110
+ if ("lastChannel" in update) {
111
+ state.update.lastChannel = expectOptionalUpdateChannel(update.lastChannel, "update.lastChannel");
112
+ }
113
+ if ("lastLocalVersion" in update) {
114
+ state.update.lastLocalVersion = expectOptionalString(update.lastLocalVersion, "update.lastLocalVersion");
115
+ }
116
+ if ("lastRemoteVersion" in update) {
117
+ state.update.lastRemoteVersion = expectOptionalString(update.lastRemoteVersion, "update.lastRemoteVersion");
118
+ }
119
+ if ("lastError" in update) {
120
+ state.update.lastError = expectOptionalString(update.lastError, "update.lastError");
121
+ }
122
+ }
123
+ if ("protect" in raw) {
124
+ const protect = expectObject(raw.protect, "protect");
125
+ if ("tools" in protect) {
126
+ const tools = expectObject(protect.tools, "protect.tools");
127
+ for (const [key, value] of Object.entries(tools)) {
128
+ if (!isProtectTool(key)) {
129
+ throw new Error(`Failed to load automation state: protect.tools.${key} is not supported`);
130
+ }
131
+ state.protect.tools[key] = parseProtectToolRuntimeState(value, `protect.tools.${key}`);
132
+ }
133
+ }
134
+ }
135
+ return state;
136
+ }
137
+ function parseProtectToolRuntimeState(value, path) {
138
+ const toolState = expectObject(value, path);
139
+ const parsed = createDefaultProtectToolRuntimeState();
140
+ if ("installed" in toolState) {
141
+ parsed.installed = expectBoolean(toolState.installed, `${path}.installed`);
142
+ }
143
+ if ("suspended" in toolState) {
144
+ parsed.suspended = expectBoolean(toolState.suspended, `${path}.suspended`);
145
+ }
146
+ if ("managedPath" in toolState) {
147
+ parsed.managedPath = expectOptionalString(toolState.managedPath, `${path}.managedPath`);
148
+ }
149
+ if ("backupPath" in toolState) {
150
+ parsed.backupPath = expectOptionalString(toolState.backupPath, `${path}.backupPath`);
151
+ }
152
+ if ("originalCommandPath" in toolState) {
153
+ parsed.originalCommandPath = expectOptionalString(
154
+ toolState.originalCommandPath,
155
+ `${path}.originalCommandPath`
156
+ );
157
+ }
158
+ if ("lastError" in toolState) {
159
+ parsed.lastError = expectOptionalString(toolState.lastError, `${path}.lastError`);
160
+ }
161
+ if ("lastChangedAt" in toolState) {
162
+ parsed.lastChangedAt = expectOptionalFiniteNumber(toolState.lastChangedAt, `${path}.lastChangedAt`);
163
+ }
164
+ return parsed;
165
+ }
166
+ async function recoverAutomationState(path, error) {
167
+ const message = error instanceof Error ? error.message : String(error);
168
+ const backupPath = `${path}.corrupt-${Date.now()}`;
169
+ let recoveredFrom;
170
+ try {
171
+ await rename(path, backupPath);
172
+ recoveredFrom = backupPath;
173
+ } catch {
174
+ recoveredFrom = path;
175
+ }
176
+ return {
177
+ exists: false,
178
+ path,
179
+ recoveredFrom,
180
+ recoveryReason: message,
181
+ state: createDefaultAutomationState()
182
+ };
183
+ }
184
+ function expectUpdateCheckResult(value, path) {
185
+ if (value !== "available" && value !== "failed" && value !== "never" && value !== "skipped" && value !== "up-to-date" && value !== "updated") {
186
+ throw new Error(`Failed to load automation state: ${path} must be a valid update result`);
187
+ }
188
+ return value;
189
+ }
190
+ function expectBoolean(value, path) {
191
+ if (typeof value !== "boolean") {
192
+ throw new Error(`Failed to load automation state: ${path} must be a boolean`);
193
+ }
194
+ return value;
195
+ }
196
+ function expectObject(value, path) {
197
+ if (!isRecord(value)) {
198
+ throw new Error(`Failed to load automation state: ${path} must be an object`);
199
+ }
200
+ return value;
201
+ }
202
+ function expectOptionalFiniteNumber(value, path) {
203
+ if (value === void 0) {
204
+ return void 0;
205
+ }
206
+ if (typeof value !== "number" || !Number.isFinite(value)) {
207
+ throw new Error(`Failed to load automation state: ${path} must be a finite number`);
208
+ }
209
+ return value;
210
+ }
211
+ function expectOptionalString(value, path) {
212
+ if (value === void 0) {
213
+ return void 0;
214
+ }
215
+ if (typeof value !== "string") {
216
+ throw new Error(`Failed to load automation state: ${path} must be a string`);
217
+ }
218
+ return value;
219
+ }
220
+ function expectOptionalUpdateChannel(value, path) {
221
+ if (value === void 0) {
222
+ return void 0;
223
+ }
224
+ if (typeof value !== "string" || !isUpdateChannel(value)) {
225
+ throw new Error(`Failed to load automation state: ${path} must be one of ${UPDATE_CHANNELS_LABEL}`);
226
+ }
227
+ return value;
228
+ }
229
+ function isRecord(value) {
230
+ return !!value && typeof value === "object" && !Array.isArray(value);
231
+ }
232
+ function expandHomePath(path) {
233
+ if (path === "~") {
234
+ return homedir();
235
+ }
236
+ if (path.startsWith("~/")) {
237
+ return join(homedir(), path.slice(2));
238
+ }
239
+ return path;
240
+ }
241
+ var UPDATE_CHANNELS_LABEL = "latest, next";
242
+
243
+ // src/config.ts
244
+ import { chmodSync as chmodSync3, existsSync as existsSync3 } from "fs";
245
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
246
+ import { homedir as homedir3 } from "os";
247
+ import { dirname as dirname3, join as join3 } from "path";
248
+
249
+ // src/ais/state.ts
250
+ import { createHash } from "crypto";
251
+ import { chmodSync as chmodSync2, existsSync as existsSync2 } from "fs";
252
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
253
+ import { homedir as homedir2 } from "os";
254
+ import { dirname as dirname2, join as join2 } from "path";
255
+
256
+ // src/vault/types.ts
257
+ var SECRET_TYPES = [
258
+ "PASSWORD",
259
+ "APIKEY",
260
+ "DBCONN",
261
+ "PRIVATE_KEY",
262
+ "BEARER_TOKEN",
263
+ "JWT",
264
+ "GENERIC"
265
+ ];
266
+ var SECRET_SOURCES = ["manual", "argv", "stdin", "proxy"];
267
+ function isSecretType(value) {
268
+ return typeof value === "string" && SECRET_TYPES.includes(value);
269
+ }
270
+ function isSecretSource(value) {
271
+ return typeof value === "string" && SECRET_SOURCES.includes(value);
272
+ }
273
+
274
+ // src/ais/state.ts
275
+ var STATE_FILE_MODE2 = 384;
276
+ var DEFAULT_AIS_STATE_PATH_DISPLAY = "~/.ais/ais-state.json";
277
+ function createDefaultAisState() {
278
+ return {
279
+ excludedRecordIds: [],
280
+ excludedTypes: [],
281
+ recentRecords: []
282
+ };
283
+ }
284
+ function buildAisRecordId(secret) {
285
+ return createHash("sha256").update(secret).digest("hex").slice(0, 12);
286
+ }
287
+ function resolveAisStatePath(path) {
288
+ return expandHomePath2(path ?? DEFAULT_AIS_STATE_PATH_DISPLAY);
289
+ }
290
+ var AisStore = class {
291
+ constructor(options = {}) {
292
+ this.options = options;
293
+ }
294
+ dirty = false;
295
+ loaded = false;
296
+ state = createDefaultAisState();
297
+ async load() {
298
+ if (this.loaded) {
299
+ return this.getState();
300
+ }
301
+ const statePath = this.getPath();
302
+ if (!existsSync2(statePath)) {
303
+ this.loaded = true;
304
+ return this.getState();
305
+ }
306
+ let parsed;
307
+ try {
308
+ parsed = JSON.parse(await readFile2(statePath, "utf8"));
309
+ } catch (error) {
310
+ const message = error instanceof Error ? error.message : String(error);
311
+ throw new Error(`Failed to load AIS state: ${message}`);
312
+ }
313
+ this.state = mergeAisState(parsed);
314
+ this.loaded = true;
315
+ return this.getState();
316
+ }
317
+ async save() {
318
+ if (!this.loaded || !this.dirty) {
319
+ return;
320
+ }
321
+ const statePath = this.getPath();
322
+ const payload = `${JSON.stringify(this.state, null, 2)}
323
+ `;
324
+ await mkdir2(dirname2(statePath), { recursive: true });
325
+ await writeFile2(statePath, payload, { mode: STATE_FILE_MODE2 });
326
+ chmodSync2(statePath, STATE_FILE_MODE2);
327
+ this.dirty = false;
328
+ }
329
+ getPath() {
330
+ return resolveAisStatePath(this.options.path);
331
+ }
332
+ getState() {
333
+ return {
334
+ excludedRecordIds: [...this.state.excludedRecordIds],
335
+ excludedTypes: [...this.state.excludedTypes],
336
+ recentRecords: this.state.recentRecords.map((record) => ({ ...record }))
337
+ };
338
+ }
339
+ isExcluded(secret, type) {
340
+ this.assertLoaded();
341
+ return this.state.excludedTypes.includes(type) || this.state.excludedRecordIds.includes(buildAisRecordId(secret));
342
+ }
343
+ recordSecret(secret, type, options) {
344
+ this.assertLoaded();
345
+ const id = buildAisRecordId(secret);
346
+ const timestamp = options.timestamp ?? Date.now();
347
+ const preview = buildPreview(secret);
348
+ const existing = this.state.recentRecords.find((record2) => record2.id === id);
349
+ if (existing) {
350
+ existing.lastSeenAt = timestamp;
351
+ existing.seenCount += 1;
352
+ existing.type = type;
353
+ existing.source = options.source;
354
+ existing.preview = preview;
355
+ existing.name = normalizeName(options.name) ?? existing.name;
356
+ this.sortRecentRecords();
357
+ this.dirty = true;
358
+ return { ...existing };
359
+ }
360
+ const record = {
361
+ id,
362
+ createdAt: timestamp,
363
+ lastSeenAt: timestamp,
364
+ name: normalizeName(options.name),
365
+ preview,
366
+ seenCount: 1,
367
+ source: options.source,
368
+ type
369
+ };
370
+ this.state.recentRecords.unshift(record);
371
+ this.trimRecentRecords();
372
+ this.sortRecentRecords();
373
+ this.dirty = true;
374
+ return { ...record };
375
+ }
376
+ setRecordExcluded(id, excluded) {
377
+ this.assertLoaded();
378
+ const normalizedId = id.trim();
379
+ if (!this.state.recentRecords.some((record) => record.id === normalizedId)) {
380
+ return false;
381
+ }
382
+ const nextIds = new Set(this.state.excludedRecordIds);
383
+ const hadValue = nextIds.has(normalizedId);
384
+ if (excluded) {
385
+ nextIds.add(normalizedId);
386
+ } else {
387
+ nextIds.delete(normalizedId);
388
+ }
389
+ if (hadValue === excluded) {
390
+ return true;
391
+ }
392
+ this.state.excludedRecordIds = Array.from(nextIds).sort();
393
+ this.dirty = true;
394
+ return true;
395
+ }
396
+ setTypeExcluded(type, excluded) {
397
+ this.assertLoaded();
398
+ const nextTypes = new Set(this.state.excludedTypes);
399
+ const hadValue = nextTypes.has(type);
400
+ if (excluded) {
401
+ nextTypes.add(type);
402
+ } else {
403
+ nextTypes.delete(type);
404
+ }
405
+ if (hadValue === excluded) {
406
+ return;
407
+ }
408
+ this.state.excludedTypes = Array.from(nextTypes).sort();
409
+ this.dirty = true;
410
+ }
411
+ assertLoaded() {
412
+ if (!this.loaded) {
413
+ throw new Error("AIS state must be loaded before use.");
414
+ }
415
+ }
416
+ sortRecentRecords() {
417
+ this.state.recentRecords.sort((left, right) => right.lastSeenAt - left.lastSeenAt);
418
+ }
419
+ trimRecentRecords() {
420
+ const recentLimit = Math.max(1, this.options.recentLimit ?? 20);
421
+ if (this.state.recentRecords.length > recentLimit) {
422
+ this.state.recentRecords.length = recentLimit;
423
+ }
424
+ }
425
+ };
426
+ function buildPreview(secret) {
427
+ const singleLine = secret.replace(/\s+/g, " ").trim();
428
+ if (singleLine.length <= 6) {
429
+ return `${singleLine.slice(0, 1)}***${singleLine.slice(-1)}`;
430
+ }
431
+ if (singleLine.length <= 12) {
432
+ return `${singleLine.slice(0, 2)}***${singleLine.slice(-2)}`;
433
+ }
434
+ return `${singleLine.slice(0, 4)}***${singleLine.slice(-4)}`;
435
+ }
436
+ function mergeAisState(raw) {
437
+ if (!isRecord2(raw)) {
438
+ throw new Error("Failed to load AIS state: root must be an object");
439
+ }
440
+ const state = createDefaultAisState();
441
+ if ("excludedRecordIds" in raw) {
442
+ if (!Array.isArray(raw.excludedRecordIds) || !raw.excludedRecordIds.every((value) => typeof value === "string")) {
443
+ throw new Error("Failed to load AIS state: excludedRecordIds must be a string array");
444
+ }
445
+ state.excludedRecordIds = raw.excludedRecordIds.map((value) => value.trim()).filter(Boolean);
446
+ }
447
+ if ("excludedTypes" in raw) {
448
+ if (!Array.isArray(raw.excludedTypes) || !raw.excludedTypes.every(isSecretType)) {
449
+ throw new Error("Failed to load AIS state: excludedTypes must contain valid secret types");
450
+ }
451
+ state.excludedTypes = [...raw.excludedTypes];
452
+ }
453
+ if ("recentRecords" in raw) {
454
+ if (!Array.isArray(raw.recentRecords)) {
455
+ throw new Error("Failed to load AIS state: recentRecords must be an array");
456
+ }
457
+ state.recentRecords = raw.recentRecords.map((value, index) => parseRecentRecord(value, index));
458
+ }
459
+ return state;
460
+ }
461
+ function parseRecentRecord(value, index) {
462
+ if (!isRecord2(value)) {
463
+ throw new Error(`Failed to load AIS state: recentRecords[${index}] must be an object`);
464
+ }
465
+ const id = expectString(value.id, `recentRecords[${index}].id`);
466
+ const createdAt = expectNumber(value.createdAt, `recentRecords[${index}].createdAt`);
467
+ const lastSeenAt = expectNumber(value.lastSeenAt, `recentRecords[${index}].lastSeenAt`);
468
+ const preview = expectString(value.preview, `recentRecords[${index}].preview`);
469
+ const seenCount = expectNumber(value.seenCount, `recentRecords[${index}].seenCount`);
470
+ const source = expectSecretSource(value.source, `recentRecords[${index}].source`);
471
+ const type = expectSecretType(value.type, `recentRecords[${index}].type`);
472
+ return {
473
+ id,
474
+ createdAt,
475
+ lastSeenAt,
476
+ name: normalizeName(expectOptionalString2(value.name, `recentRecords[${index}].name`)),
477
+ preview,
478
+ seenCount,
479
+ source,
480
+ type
481
+ };
482
+ }
483
+ function expectNumber(value, path) {
484
+ if (typeof value !== "number" || !Number.isFinite(value)) {
485
+ throw new Error(`Failed to load AIS state: ${path} must be a finite number`);
486
+ }
487
+ return value;
488
+ }
489
+ function expectOptionalString2(value, path) {
490
+ if (value === void 0) {
491
+ return void 0;
492
+ }
493
+ return expectString(value, path);
494
+ }
495
+ function expectSecretSource(value, path) {
496
+ if (!isSecretSource(value)) {
497
+ throw new Error(`Failed to load AIS state: ${path} must be a valid source`);
498
+ }
499
+ return value;
500
+ }
501
+ function expectSecretType(value, path) {
502
+ if (!isSecretType(value)) {
503
+ throw new Error(`Failed to load AIS state: ${path} must be a valid type`);
504
+ }
505
+ return value;
506
+ }
507
+ function expectString(value, path) {
508
+ if (typeof value !== "string") {
509
+ throw new Error(`Failed to load AIS state: ${path} must be a string`);
510
+ }
511
+ return value;
512
+ }
513
+ function isRecord2(value) {
514
+ return !!value && typeof value === "object" && !Array.isArray(value);
515
+ }
516
+ function normalizeName(name) {
517
+ if (name === void 0) {
518
+ return void 0;
519
+ }
520
+ const trimmed = name.trim();
521
+ return trimmed.length === 0 ? void 0 : trimmed;
522
+ }
523
+ function expandHomePath2(path) {
524
+ if (path === "~") {
525
+ return homedir2();
526
+ }
527
+ if (path.startsWith("~/")) {
528
+ return join2(homedir2(), path.slice(2));
529
+ }
530
+ return path;
531
+ }
532
+
533
+ // src/config.ts
534
+ var CONFIG_FILE_MODE = 384;
535
+ var DEFAULT_CONFIG_PATH = join3(homedir3(), ".ais", "config.json");
536
+ var DEFAULT_VAULT_PATH_DISPLAY = "~/.ais/vault.enc";
537
+ function createDefaultConfig() {
538
+ return {
539
+ ais: {
540
+ recentLimit: 20,
541
+ statePath: DEFAULT_AIS_STATE_PATH_DISPLAY
542
+ },
543
+ automation: {
544
+ statePath: DEFAULT_AUTOMATION_STATE_PATH_DISPLAY
545
+ },
546
+ customPatterns: [],
547
+ detection: {
548
+ patterns: true,
549
+ context: true,
550
+ entropy: false,
551
+ entropyThreshold: 4
552
+ },
553
+ display: {
554
+ debug: false
555
+ },
556
+ protect: {
557
+ enabled: true,
558
+ tools: {
559
+ claude: true,
560
+ codex: true,
561
+ openclaw: true
562
+ }
563
+ },
564
+ storage: {
565
+ persistSecrets: false,
566
+ vaultPath: DEFAULT_VAULT_PATH_DISPLAY
567
+ },
568
+ update: {
569
+ channel: "latest",
570
+ checkIntervalMinutes: 1440,
571
+ enabled: true,
572
+ silent: true
573
+ }
574
+ };
575
+ }
576
+ function expandHomePath3(path) {
577
+ if (path === "~") {
578
+ return homedir3();
579
+ }
580
+ if (path.startsWith("~/")) {
581
+ return join3(homedir3(), path.slice(2));
582
+ }
583
+ return path;
584
+ }
585
+ function resolveConfigPath(path) {
586
+ return expandHomePath3(path ?? DEFAULT_CONFIG_PATH);
587
+ }
588
+ async function loadConfig(path) {
589
+ const resolvedPath = resolveConfigPath(path);
590
+ if (!existsSync3(resolvedPath)) {
591
+ return {
592
+ config: createDefaultConfig(),
593
+ exists: false,
594
+ path: resolvedPath
595
+ };
596
+ }
597
+ let parsed;
598
+ try {
599
+ parsed = JSON.parse(await readFile3(resolvedPath, "utf8"));
600
+ } catch (error) {
601
+ const message = error instanceof Error ? error.message : String(error);
602
+ throw new Error(`Failed to load config: ${message}`);
603
+ }
604
+ return {
605
+ config: mergeConfig(parsed),
606
+ exists: true,
607
+ path: resolvedPath
608
+ };
609
+ }
610
+ async function saveConfig(config, path) {
611
+ const resolvedPath = resolveConfigPath(path);
612
+ const payload = `${JSON.stringify(config, null, 2)}
613
+ `;
614
+ await mkdir3(dirname3(resolvedPath), { recursive: true });
615
+ await writeFile3(resolvedPath, payload, { mode: CONFIG_FILE_MODE });
616
+ chmodSync3(resolvedPath, CONFIG_FILE_MODE);
617
+ }
618
+ function mergeConfig(raw) {
619
+ if (!isRecord3(raw)) {
620
+ throw new Error("Failed to load config: root must be an object");
621
+ }
622
+ const config = createDefaultConfig();
623
+ if ("detection" in raw) {
624
+ const detection = expectObject2(raw.detection, "detection");
625
+ if ("patterns" in detection) {
626
+ config.detection.patterns = expectBoolean2(detection.patterns, "detection.patterns");
627
+ }
628
+ if ("context" in detection) {
629
+ config.detection.context = expectBoolean2(detection.context, "detection.context");
630
+ }
631
+ if ("entropy" in detection) {
632
+ config.detection.entropy = expectBoolean2(detection.entropy, "detection.entropy");
633
+ }
634
+ if ("entropyThreshold" in detection) {
635
+ config.detection.entropyThreshold = expectFiniteNumber(
636
+ detection.entropyThreshold,
637
+ "detection.entropyThreshold"
638
+ );
639
+ }
640
+ }
641
+ if ("ais" in raw) {
642
+ const ais = expectObject2(raw.ais, "ais");
643
+ if ("recentLimit" in ais) {
644
+ config.ais.recentLimit = expectPositiveInteger(ais.recentLimit, "ais.recentLimit");
645
+ }
646
+ if ("statePath" in ais) {
647
+ config.ais.statePath = expectString2(ais.statePath, "ais.statePath");
648
+ }
649
+ }
650
+ if ("automation" in raw) {
651
+ const automation = expectObject2(raw.automation, "automation");
652
+ if ("statePath" in automation) {
653
+ config.automation.statePath = expectString2(automation.statePath, "automation.statePath");
654
+ }
655
+ }
656
+ if ("display" in raw) {
657
+ const display = expectObject2(raw.display, "display");
658
+ if ("debug" in display) {
659
+ config.display.debug = expectBoolean2(display.debug, "display.debug");
660
+ }
661
+ }
662
+ if ("storage" in raw) {
663
+ const storage = expectObject2(raw.storage, "storage");
664
+ if ("persistSecrets" in storage) {
665
+ config.storage.persistSecrets = expectBoolean2(storage.persistSecrets, "storage.persistSecrets");
666
+ }
667
+ if ("vaultPath" in storage) {
668
+ config.storage.vaultPath = expectString2(storage.vaultPath, "storage.vaultPath");
669
+ }
670
+ }
671
+ if ("update" in raw) {
672
+ const update = expectObject2(raw.update, "update");
673
+ if ("enabled" in update) {
674
+ config.update.enabled = expectBoolean2(update.enabled, "update.enabled");
675
+ }
676
+ if ("channel" in update) {
677
+ config.update.channel = expectUpdateChannel(update.channel, "update.channel");
678
+ }
679
+ if ("checkIntervalMinutes" in update) {
680
+ config.update.checkIntervalMinutes = expectPositiveInteger(
681
+ update.checkIntervalMinutes,
682
+ "update.checkIntervalMinutes"
683
+ );
684
+ }
685
+ if ("silent" in update) {
686
+ config.update.silent = expectBoolean2(update.silent, "update.silent");
687
+ }
688
+ }
689
+ if ("protect" in raw) {
690
+ const protect = expectObject2(raw.protect, "protect");
691
+ if ("enabled" in protect) {
692
+ config.protect.enabled = expectBoolean2(protect.enabled, "protect.enabled");
693
+ }
694
+ if ("tools" in protect) {
695
+ const tools = expectObject2(protect.tools, "protect.tools");
696
+ if ("claude" in tools) {
697
+ config.protect.tools.claude = expectBoolean2(tools.claude, "protect.tools.claude");
698
+ }
699
+ if ("codex" in tools) {
700
+ config.protect.tools.codex = expectBoolean2(tools.codex, "protect.tools.codex");
701
+ }
702
+ if ("openclaw" in tools) {
703
+ config.protect.tools.openclaw = expectBoolean2(tools.openclaw, "protect.tools.openclaw");
704
+ }
705
+ }
706
+ }
707
+ if ("customPatterns" in raw) {
708
+ if (!Array.isArray(raw.customPatterns)) {
709
+ throw new Error("Failed to load config: customPatterns must be an array");
710
+ }
711
+ config.customPatterns = raw.customPatterns.map((value, index) => parseCustomPattern(value, index));
712
+ }
713
+ return config;
714
+ }
715
+ function parseCustomPattern(value, index) {
716
+ const pattern = expectObject2(value, `customPatterns[${index}]`);
717
+ const id = expectString2(pattern.id, `customPatterns[${index}].id`);
718
+ const regex = expectString2(pattern.regex, `customPatterns[${index}].regex`);
719
+ const type = expectString2(pattern.type, `customPatterns[${index}].type`);
720
+ if (!isSecretType(type)) {
721
+ throw new Error(`Failed to load config: customPatterns[${index}].type is invalid`);
722
+ }
723
+ try {
724
+ new RegExp(regex, "g");
725
+ } catch (error) {
726
+ const message = error instanceof Error ? error.message : String(error);
727
+ throw new Error(`Failed to load config: customPatterns[${index}].regex is invalid: ${message}`);
728
+ }
729
+ return {
730
+ id,
731
+ regex,
732
+ type
733
+ };
734
+ }
735
+ function expectBoolean2(value, path) {
736
+ if (typeof value !== "boolean") {
737
+ throw new Error(`Failed to load config: ${path} must be a boolean`);
738
+ }
739
+ return value;
740
+ }
741
+ function expectFiniteNumber(value, path) {
742
+ if (typeof value !== "number" || !Number.isFinite(value)) {
743
+ throw new Error(`Failed to load config: ${path} must be a finite number`);
744
+ }
745
+ return value;
746
+ }
747
+ function expectPositiveInteger(value, path) {
748
+ const parsed = expectFiniteNumber(value, path);
749
+ if (!Number.isInteger(parsed) || parsed < 1) {
750
+ throw new Error(`Failed to load config: ${path} must be a positive integer`);
751
+ }
752
+ return parsed;
753
+ }
754
+ function expectUpdateChannel(value, path) {
755
+ if (typeof value !== "string" || !isUpdateChannel(value)) {
756
+ throw new Error(`Failed to load config: ${path} must be "latest" or "next"`);
757
+ }
758
+ return value;
759
+ }
760
+ function expectObject2(value, path) {
761
+ if (!isRecord3(value)) {
762
+ throw new Error(`Failed to load config: ${path} must be an object`);
763
+ }
764
+ return value;
765
+ }
766
+ function expectString2(value, path) {
767
+ if (typeof value !== "string") {
768
+ throw new Error(`Failed to load config: ${path} must be a string`);
769
+ }
770
+ return value;
771
+ }
772
+ function isRecord3(value) {
773
+ return !!value && typeof value === "object" && !Array.isArray(value);
774
+ }
775
+
776
+ // src/protect/index.ts
777
+ import { spawn } from "child_process";
778
+ import { constants, existsSync as existsSync4 } from "fs";
779
+ import { access, chmod, lstat, mkdir as mkdir4, readFile as readFile4, readlink, rename as rename2, rm, writeFile as writeFile4 } from "fs/promises";
780
+ import { homedir as homedir4 } from "os";
781
+ import { basename, delimiter, dirname as dirname4, join as join4, resolve } from "path";
782
+ import { fileURLToPath } from "url";
783
+ var EXECUTABLE_MODE = 493;
784
+ var MANAGED_WRAPPER_MARKER = "# AIS protect wrapper";
785
+ var SHELL_BLOCK_START = "# >>> AIS protect >>>";
786
+ var SHELL_BLOCK_END = "# <<< AIS protect <<<";
787
+ async function syncProtectRuntime(config, state, options = {}) {
788
+ const context = createContext(options);
789
+ const nextState = cloneAutomationState(state);
790
+ const warnings = [];
791
+ const errors = [];
792
+ let managedBinNeeded = false;
793
+ for (const tool of PROTECT_TOOLS) {
794
+ const desired = config.protect.enabled && config.protect.tools[tool];
795
+ const result = desired ? await installToolTakeover(tool, nextState.protect.tools[tool], context) : await removeToolTakeover(tool, nextState.protect.tools[tool], context, true);
796
+ nextState.protect.tools[tool] = result.runtime;
797
+ managedBinNeeded ||= result.usesManagedBin;
798
+ if (result.warning) {
799
+ warnings.push(result.warning);
800
+ }
801
+ if (result.error) {
802
+ errors.push(result.error);
803
+ }
804
+ }
805
+ const shellChanged = managedBinNeeded ? await ensureShellBlock(context) : await removeShellBlock(context);
806
+ return {
807
+ changed: shellChanged || hasStateChanged(state, nextState),
808
+ errors,
809
+ state: nextState,
810
+ warnings
811
+ };
812
+ }
813
+ async function refreshProtectRuntime(state, options = {}) {
814
+ const context = createContext(options);
815
+ const nextState = cloneAutomationState(state);
816
+ const warnings = [];
817
+ for (const tool of PROTECT_TOOLS) {
818
+ const current = nextState.protect.tools[tool];
819
+ const inspection = await inspectToolRuntime(tool, current, context);
820
+ nextState.protect.tools[tool] = inspection.runtime;
821
+ if (inspection.warning) {
822
+ warnings.push(inspection.warning);
823
+ }
824
+ }
825
+ return {
826
+ changed: hasStateChanged(state, nextState),
827
+ errors: [],
828
+ state: nextState,
829
+ warnings
830
+ };
831
+ }
832
+ async function restoreProtectRuntime(config, state, options = {}) {
833
+ const context = createContext(options);
834
+ const nextState = cloneAutomationState(state);
835
+ const warnings = [];
836
+ const errors = [];
837
+ for (const tool of PROTECT_TOOLS) {
838
+ const result = await removeToolTakeover(tool, nextState.protect.tools[tool], context, false);
839
+ nextState.protect.tools[tool] = result.runtime;
840
+ if (result.warning) {
841
+ warnings.push(result.warning);
842
+ }
843
+ if (result.error) {
844
+ errors.push(result.error);
845
+ }
846
+ }
847
+ const shellChanged = await removeShellBlock(context);
848
+ const nextConfig = structuredClone(config);
849
+ nextConfig.protect.enabled = false;
850
+ nextConfig.protect.tools.claude = false;
851
+ nextConfig.protect.tools.codex = false;
852
+ nextConfig.protect.tools.openclaw = false;
853
+ return {
854
+ changed: shellChanged || hasStateChanged(state, nextState) || JSON.stringify(config.protect) !== JSON.stringify(nextConfig.protect),
855
+ config: nextConfig,
856
+ errors,
857
+ state: nextState,
858
+ warnings
859
+ };
860
+ }
861
+ async function resolveProtectedCommand(command, env = process.env, options = {}) {
862
+ if (env.AIS_PROTECT_WRAPPER_ACTIVE !== "1" || env.AIS_PROTECT_TOOL !== command || !PROTECT_TOOLS.includes(command)) {
863
+ return command;
864
+ }
865
+ const explicitCommand = env.AIS_PROTECT_REAL_COMMAND?.trim();
866
+ const wrapperPath = env.AIS_PROTECT_WRAPPER_PATH?.trim();
867
+ if (explicitCommand && explicitCommand !== wrapperPath && await isExecutablePath(explicitCommand)) {
868
+ return explicitCommand;
869
+ }
870
+ const homeDir = options.homeDir ?? env.HOME ?? homedir4();
871
+ const managedBinDir = env.AIS_PROTECT_WRAPPER_DIR?.trim() || join4(homeDir, ".ais", "bin");
872
+ const found = await findCommandPaths(command, env.PATH ?? process.env.PATH ?? "", {
873
+ excludeDirs: [managedBinDir],
874
+ excludePaths: wrapperPath ? [wrapperPath] : []
875
+ });
876
+ if (found.paths[0]) {
877
+ return found.paths[0];
878
+ }
879
+ throw new Error(
880
+ `Protected command ${command} does not have a real target yet. Install the original tool first, or run "ais protect off ${command}".`
881
+ );
882
+ }
883
+ function createContext(options) {
884
+ const env = options.env ?? process.env;
885
+ const homeDir = options.homeDir ?? env.HOME ?? homedir4();
886
+ return {
887
+ aisCliPath: options.aisCliPath ?? resolveManagedCliPath(),
888
+ backupRootDir: join4(homeDir, ".ais", "backups"),
889
+ env,
890
+ homeDir,
891
+ managedBinDir: join4(homeDir, ".ais", "bin"),
892
+ nodePath: options.nodePath ?? process.execPath,
893
+ now: options.now ?? Date.now,
894
+ shellPath: options.shellPath ?? env.SHELL,
895
+ shellRunner: options.shellRunner ?? runShellCommand
896
+ };
897
+ }
898
+ function resolveManagedCliPath() {
899
+ const candidatePaths = [
900
+ fileURLToPath(new URL("../../dist/cli.js", import.meta.url)),
901
+ fileURLToPath(new URL("../cli.js", import.meta.url)),
902
+ process.argv[1] ? resolve(process.argv[1]) : void 0
903
+ ].filter((value) => typeof value === "string" && value.length > 0);
904
+ const foundPath = candidatePaths.find((candidatePath) => existsSync4(candidatePath));
905
+ if (foundPath) {
906
+ return foundPath;
907
+ }
908
+ return resolve(join4(process.cwd(), "dist", "cli.js"));
909
+ }
910
+ async function installToolTakeover(tool, runtime, context) {
911
+ const commandInfo = await findCommandPaths(tool, context.env.PATH ?? process.env.PATH ?? "", {
912
+ context,
913
+ excludeDirs: [context.managedBinDir]
914
+ });
915
+ const collisionMessage = formatCollisionMessage(tool, commandInfo.collision);
916
+ if (runtime.installed && runtime.managedPath && await isManagedWrapper(runtime.managedPath)) {
917
+ if (runtime.managedPath.startsWith(context.managedBinDir)) {
918
+ const rewritten2 = await installPrependTakeover(tool, runtime, commandInfo, context);
919
+ if (collisionMessage && !rewritten2.warning) {
920
+ rewritten2.warning = collisionMessage;
921
+ }
922
+ return rewritten2;
923
+ }
924
+ const rewritten = await installInPlaceTakeover(
925
+ tool,
926
+ runtime.managedPath,
927
+ runtime.originalCommandPath ?? runtime.managedPath,
928
+ runtime.backupPath,
929
+ context
930
+ );
931
+ if (collisionMessage && !rewritten.warning) {
932
+ rewritten.warning = collisionMessage;
933
+ }
934
+ return rewritten;
935
+ }
936
+ if (commandInfo.firstPath && await shouldUseInPlaceTakeover(commandInfo.firstPath, context.homeDir)) {
937
+ const installed = await installInPlaceTakeover(tool, commandInfo.firstPath, commandInfo.firstPath, void 0, context);
938
+ if (collisionMessage && !installed.warning) {
939
+ installed.warning = collisionMessage;
940
+ }
941
+ return installed;
942
+ }
943
+ const prepended = await installPrependTakeover(tool, runtime, commandInfo, context);
944
+ if (collisionMessage && !prepended.warning) {
945
+ prepended.warning = collisionMessage;
946
+ }
947
+ return prepended;
948
+ }
949
+ async function installInPlaceTakeover(tool, managedPath, originalCommandPath, currentBackupPath, context) {
950
+ const backupPath = currentBackupPath ?? join4(context.backupRootDir, tool, basename(originalCommandPath));
951
+ const wrapper = createWrapperScript(tool, managedPath, context, backupPath);
952
+ let warning;
953
+ if (currentBackupPath && !existsSync4(currentBackupPath)) {
954
+ warning = `${tool}: backup target is missing, fallback lookup will be used until it is rebuilt.`;
955
+ }
956
+ if (await isManagedWrapper(managedPath)) {
957
+ await writeExecutableFile(managedPath, wrapper);
958
+ return {
959
+ runtime: buildRuntimeState({
960
+ backupPath,
961
+ installed: true,
962
+ lastError: warning,
963
+ managedPath,
964
+ originalCommandPath,
965
+ suspended: false
966
+ }, context),
967
+ usesManagedBin: false,
968
+ warning
969
+ };
970
+ }
971
+ await mkdir4(dirname4(backupPath), { recursive: true });
972
+ if (existsSync4(backupPath)) {
973
+ await rm(backupPath, { force: true, recursive: true });
974
+ }
975
+ let renamed = false;
976
+ try {
977
+ await rename2(managedPath, backupPath);
978
+ renamed = true;
979
+ await writeExecutableFile(managedPath, wrapper);
980
+ return {
981
+ runtime: buildRuntimeState({
982
+ backupPath,
983
+ installed: true,
984
+ lastError: warning,
985
+ managedPath,
986
+ originalCommandPath,
987
+ suspended: false
988
+ }, context),
989
+ usesManagedBin: false,
990
+ warning
991
+ };
992
+ } catch (error) {
993
+ if (renamed) {
994
+ await rm(managedPath, { force: true }).catch(() => void 0);
995
+ await rename2(backupPath, managedPath).catch(() => void 0);
996
+ }
997
+ return {
998
+ error: `${tool}: failed to install in-place takeover (${toErrorMessage(error)})`,
999
+ runtime: buildRuntimeState({
1000
+ installed: false,
1001
+ lastError: `install failed: ${toErrorMessage(error)}`,
1002
+ originalCommandPath,
1003
+ suspended: false
1004
+ }, context),
1005
+ usesManagedBin: false
1006
+ };
1007
+ }
1008
+ }
1009
+ async function installPrependTakeover(tool, runtime, commandInfo, context) {
1010
+ if (runtime.installed && runtime.managedPath && !runtime.managedPath.startsWith(context.managedBinDir)) {
1011
+ const removed = await removeToolTakeover(tool, runtime, context, false);
1012
+ if (removed.error) {
1013
+ return removed;
1014
+ }
1015
+ }
1016
+ const managedPath = join4(context.managedBinDir, tool);
1017
+ await mkdir4(context.managedBinDir, { recursive: true });
1018
+ await writeExecutableFile(managedPath, createWrapperScript(tool, managedPath, context));
1019
+ const warningParts = [];
1020
+ if (!commandInfo.firstPath) {
1021
+ warningParts.push(`${tool}: original command is not installed yet; AIS will wait for it to appear later.`);
1022
+ }
1023
+ const collisionMessage = formatCollisionMessage(tool, commandInfo.collision);
1024
+ if (collisionMessage) {
1025
+ warningParts.push(collisionMessage);
1026
+ }
1027
+ return {
1028
+ runtime: buildRuntimeState({
1029
+ installed: true,
1030
+ lastError: warningParts.length === 0 ? void 0 : warningParts.join(" "),
1031
+ managedPath,
1032
+ originalCommandPath: commandInfo.firstPath,
1033
+ suspended: false
1034
+ }, context),
1035
+ usesManagedBin: true,
1036
+ warning: warningParts.length === 0 ? void 0 : warningParts.join(" ")
1037
+ };
1038
+ }
1039
+ async function removeToolTakeover(tool, runtime, context, keepSuspended) {
1040
+ const managedPath = runtime.managedPath;
1041
+ const backupPath = runtime.backupPath;
1042
+ const originalCommandPath = runtime.originalCommandPath;
1043
+ if (managedPath && managedPath.startsWith(context.managedBinDir)) {
1044
+ if (existsSync4(managedPath)) {
1045
+ await rm(managedPath, { force: true }).catch(() => void 0);
1046
+ }
1047
+ const commandInfo2 = await findCommandPaths(tool, context.env.PATH ?? process.env.PATH ?? "", {
1048
+ context,
1049
+ excludeDirs: [context.managedBinDir]
1050
+ });
1051
+ return {
1052
+ runtime: buildRuntimeState({
1053
+ installed: false,
1054
+ lastError: void 0,
1055
+ originalCommandPath: commandInfo2.firstPath ?? originalCommandPath,
1056
+ suspended: keepSuspended
1057
+ }, context),
1058
+ usesManagedBin: false
1059
+ };
1060
+ }
1061
+ if (managedPath && backupPath && existsSync4(backupPath)) {
1062
+ const managedIsWrapper = await isManagedWrapper(managedPath);
1063
+ try {
1064
+ if (managedIsWrapper) {
1065
+ await rm(managedPath, { force: true });
1066
+ await rename2(backupPath, originalCommandPath ?? managedPath);
1067
+ } else if (!existsSync4(originalCommandPath ?? managedPath)) {
1068
+ await rename2(backupPath, originalCommandPath ?? managedPath);
1069
+ } else {
1070
+ await rm(backupPath, { force: true, recursive: true });
1071
+ }
1072
+ return {
1073
+ runtime: buildRuntimeState({
1074
+ installed: false,
1075
+ lastError: void 0,
1076
+ originalCommandPath: originalCommandPath ?? managedPath,
1077
+ suspended: keepSuspended
1078
+ }, context),
1079
+ usesManagedBin: false
1080
+ };
1081
+ } catch (error) {
1082
+ return {
1083
+ error: `${tool}: failed to restore original command (${toErrorMessage(error)})`,
1084
+ runtime: buildRuntimeState({
1085
+ backupPath,
1086
+ installed: managedIsWrapper,
1087
+ lastError: `restore failed: ${toErrorMessage(error)}`,
1088
+ managedPath,
1089
+ originalCommandPath: originalCommandPath ?? managedPath,
1090
+ suspended: keepSuspended
1091
+ }, context),
1092
+ usesManagedBin: false
1093
+ };
1094
+ }
1095
+ }
1096
+ const commandInfo = await findCommandPaths(tool, context.env.PATH ?? process.env.PATH ?? "", {
1097
+ context,
1098
+ excludeDirs: [context.managedBinDir]
1099
+ });
1100
+ return {
1101
+ runtime: buildRuntimeState({
1102
+ installed: false,
1103
+ lastError: void 0,
1104
+ originalCommandPath: commandInfo.firstPath ?? originalCommandPath,
1105
+ suspended: keepSuspended
1106
+ }, context),
1107
+ usesManagedBin: false
1108
+ };
1109
+ }
1110
+ async function inspectToolRuntime(tool, runtime, context) {
1111
+ const nextRuntime = {
1112
+ ...runtime
1113
+ };
1114
+ const commandInfo = await findCommandPaths(tool, context.env.PATH ?? process.env.PATH ?? "", {
1115
+ context,
1116
+ excludeDirs: [context.managedBinDir],
1117
+ excludePaths: runtime.managedPath ? [runtime.managedPath] : []
1118
+ });
1119
+ if (!runtime.managedPath) {
1120
+ nextRuntime.installed = false;
1121
+ nextRuntime.originalCommandPath = commandInfo.firstPath;
1122
+ nextRuntime.lastError = void 0;
1123
+ return {
1124
+ runtime: nextRuntime,
1125
+ usesManagedBin: false
1126
+ };
1127
+ }
1128
+ const installed = await isManagedWrapper(runtime.managedPath);
1129
+ const warningParts = [];
1130
+ const collisionMessage = formatCollisionMessage(tool, commandInfo.collision);
1131
+ if (collisionMessage) {
1132
+ warningParts.push(collisionMessage);
1133
+ }
1134
+ if (runtime.managedPath.startsWith(context.managedBinDir) && !commandInfo.firstPath) {
1135
+ warningParts.push(`${tool}: original command is not installed yet.`);
1136
+ }
1137
+ if (!installed) {
1138
+ warningParts.push(`${tool}: managed entry is missing or has been replaced.`);
1139
+ }
1140
+ nextRuntime.installed = installed;
1141
+ nextRuntime.originalCommandPath = commandInfo.firstPath ?? runtime.originalCommandPath;
1142
+ nextRuntime.lastError = warningParts.length === 0 ? void 0 : warningParts.join(" ");
1143
+ return {
1144
+ runtime: nextRuntime,
1145
+ usesManagedBin: runtime.managedPath.startsWith(context.managedBinDir),
1146
+ warning: warningParts.length === 0 ? void 0 : warningParts.join(" ")
1147
+ };
1148
+ }
1149
+ async function shouldUseInPlaceTakeover(commandPath, homeDir) {
1150
+ const normalized = resolve(commandPath);
1151
+ if (!normalized.startsWith(resolve(homeDir))) {
1152
+ return false;
1153
+ }
1154
+ const lower = normalized.toLowerCase();
1155
+ if (lower.includes("/.npm-global/bin/") || lower.includes("/.npm/bin/") || lower.includes("/node_modules/.bin/")) {
1156
+ return true;
1157
+ }
1158
+ try {
1159
+ const entry = await lstat(commandPath);
1160
+ if (entry.isSymbolicLink()) {
1161
+ const linked = resolve(dirname4(commandPath), await readlink(commandPath)).toLowerCase();
1162
+ return linked.includes("/node_modules/");
1163
+ }
1164
+ } catch {
1165
+ return false;
1166
+ }
1167
+ return false;
1168
+ }
1169
+ async function findCommandPaths(tool, pathValue, options = {}) {
1170
+ const directories = pathValue.split(delimiter).map((entry) => entry.trim()).filter(Boolean).map((entry) => resolve(entry));
1171
+ const excludeDirs = new Set((options.excludeDirs ?? []).map((entry) => resolve(entry)));
1172
+ const excludePaths = new Set((options.excludePaths ?? []).filter(Boolean).map((entry) => resolve(entry)));
1173
+ const paths = [];
1174
+ for (const directory of directories) {
1175
+ if (excludeDirs.has(directory)) {
1176
+ continue;
1177
+ }
1178
+ const candidate = resolve(directory, tool);
1179
+ if (excludePaths.has(candidate)) {
1180
+ continue;
1181
+ }
1182
+ if (!await isExecutablePath(candidate)) {
1183
+ continue;
1184
+ }
1185
+ paths.push(candidate);
1186
+ }
1187
+ const shellInspection = await inspectShellCommand(tool, options.context);
1188
+ return {
1189
+ collision: shellInspection.collision,
1190
+ firstPath: paths[0],
1191
+ paths
1192
+ };
1193
+ }
1194
+ async function inspectShellCommand(tool, context) {
1195
+ if (!context?.shellPath) {
1196
+ return {};
1197
+ }
1198
+ try {
1199
+ const result = await context.shellRunner(context.shellPath, ["-lic", `type -a ${tool}`], {
1200
+ env: context.env
1201
+ });
1202
+ if (result.exitCode !== 0) {
1203
+ return {};
1204
+ }
1205
+ const firstLine = result.stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
1206
+ if (!firstLine) {
1207
+ return {};
1208
+ }
1209
+ if (/\b(alias|aliased)\b/i.test(firstLine)) {
1210
+ return { collision: "alias" };
1211
+ }
1212
+ if (/\bfunction\b/i.test(firstLine)) {
1213
+ return { collision: "function" };
1214
+ }
1215
+ } catch {
1216
+ return {};
1217
+ }
1218
+ return {};
1219
+ }
1220
+ async function ensureShellBlock(context) {
1221
+ const targets = await resolveShellTargets(context);
1222
+ const block = buildShellBlock();
1223
+ let changed = false;
1224
+ for (const target of targets) {
1225
+ const current = existsSync4(target) ? await readFile4(target, "utf8") : "";
1226
+ const next = stripShellBlock(current).replace(/\s+$/, "");
1227
+ const content = next.length === 0 ? `${block}
1228
+ ` : `${next}
1229
+
1230
+ ${block}
1231
+ `;
1232
+ if (current === content) {
1233
+ continue;
1234
+ }
1235
+ await mkdir4(dirname4(target), { recursive: true });
1236
+ await writeFile4(target, content, "utf8");
1237
+ changed = true;
1238
+ }
1239
+ return changed;
1240
+ }
1241
+ async function removeShellBlock(context) {
1242
+ const targets = await resolveShellTargets(context, true);
1243
+ let changed = false;
1244
+ for (const target of targets) {
1245
+ if (!existsSync4(target)) {
1246
+ continue;
1247
+ }
1248
+ const current = await readFile4(target, "utf8");
1249
+ const next = stripShellBlock(current).replace(/\s+$/, "");
1250
+ const content = next.length === 0 ? "" : `${next}
1251
+ `;
1252
+ if (current === content) {
1253
+ continue;
1254
+ }
1255
+ await writeFile4(target, content, "utf8");
1256
+ changed = true;
1257
+ }
1258
+ return changed;
1259
+ }
1260
+ async function resolveShellTargets(context, includeAllExisting = false) {
1261
+ const candidates = [
1262
+ join4(context.homeDir, ".zshrc"),
1263
+ join4(context.homeDir, ".zprofile"),
1264
+ join4(context.homeDir, ".bashrc"),
1265
+ join4(context.homeDir, ".bash_profile")
1266
+ ];
1267
+ const existing = candidates.filter((target) => existsSync4(target));
1268
+ if (includeAllExisting) {
1269
+ return existing;
1270
+ }
1271
+ if (existing.length > 0) {
1272
+ return existing;
1273
+ }
1274
+ const shellPath = context.shellPath ?? "";
1275
+ if (shellPath.includes("bash")) {
1276
+ return [join4(context.homeDir, ".bashrc")];
1277
+ }
1278
+ return [join4(context.homeDir, ".zshrc")];
1279
+ }
1280
+ function buildShellBlock() {
1281
+ return [
1282
+ SHELL_BLOCK_START,
1283
+ 'if [ -d "$HOME/.ais/bin" ]; then',
1284
+ ' export PATH="$HOME/.ais/bin:$PATH"',
1285
+ "fi",
1286
+ SHELL_BLOCK_END
1287
+ ].join("\n");
1288
+ }
1289
+ function stripShellBlock(content) {
1290
+ const pattern = new RegExp(`${escapeForRegExp(SHELL_BLOCK_START)}[\\s\\S]*?${escapeForRegExp(SHELL_BLOCK_END)}\\n?`, "g");
1291
+ return content.replace(pattern, "");
1292
+ }
1293
+ function escapeForRegExp(value) {
1294
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1295
+ }
1296
+ function createWrapperScript(tool, managedPath, context, realCommandPath) {
1297
+ return [
1298
+ "#!/bin/sh",
1299
+ MANAGED_WRAPPER_MARKER,
1300
+ `export AIS_PROTECT_WRAPPER_ACTIVE=${quoteForShell("1")}`,
1301
+ `export AIS_PROTECT_TOOL=${quoteForShell(tool)}`,
1302
+ `export AIS_PROTECT_WRAPPER_PATH=${quoteForShell(managedPath)}`,
1303
+ `export AIS_PROTECT_WRAPPER_DIR=${quoteForShell(context.managedBinDir)}`,
1304
+ `export AIS_PROTECT_REAL_COMMAND=${quoteForShell(realCommandPath ?? "")}`,
1305
+ `exec ${quoteForShell(context.nodePath)} ${quoteForShell(context.aisCliPath)} ${quoteForShell(tool)} "$@"`,
1306
+ ""
1307
+ ].join("\n");
1308
+ }
1309
+ function buildRuntimeState(state, context) {
1310
+ return {
1311
+ ...createDefaultProtectToolRuntimeState(),
1312
+ ...state,
1313
+ lastChangedAt: context.now()
1314
+ };
1315
+ }
1316
+ function formatCollisionMessage(tool, collision) {
1317
+ if (!collision) {
1318
+ return void 0;
1319
+ }
1320
+ return `${tool}: your current shell still has a ${collision} ahead of AIS, so direct launches may keep hitting that ${collision}.`;
1321
+ }
1322
+ function quoteForShell(value) {
1323
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1324
+ }
1325
+ async function isManagedWrapper(filePath) {
1326
+ if (!existsSync4(filePath)) {
1327
+ return false;
1328
+ }
1329
+ try {
1330
+ const content = await readFile4(filePath, "utf8");
1331
+ return content.includes(MANAGED_WRAPPER_MARKER);
1332
+ } catch {
1333
+ return false;
1334
+ }
1335
+ }
1336
+ async function isExecutablePath(path) {
1337
+ try {
1338
+ const stats = await lstat(path);
1339
+ if (stats.isDirectory()) {
1340
+ return false;
1341
+ }
1342
+ await access(path, constants.X_OK);
1343
+ return true;
1344
+ } catch {
1345
+ return false;
1346
+ }
1347
+ }
1348
+ async function writeExecutableFile(path, content) {
1349
+ await writeFile4(path, content, "utf8");
1350
+ await chmod(path, EXECUTABLE_MODE);
1351
+ }
1352
+ async function runShellCommand(command, args, options) {
1353
+ return await new Promise((resolvePromise, reject) => {
1354
+ const child = spawn(command, args, {
1355
+ env: options.env,
1356
+ stdio: ["ignore", "pipe", "pipe"]
1357
+ });
1358
+ let stdout = "";
1359
+ let stderr = "";
1360
+ child.stdout.on("data", (chunk) => {
1361
+ stdout += chunk.toString();
1362
+ });
1363
+ child.stderr.on("data", (chunk) => {
1364
+ stderr += chunk.toString();
1365
+ });
1366
+ child.on("error", reject);
1367
+ child.on("close", (exitCode) => {
1368
+ resolvePromise({
1369
+ exitCode: exitCode ?? 1,
1370
+ stderr,
1371
+ stdout
1372
+ });
1373
+ });
1374
+ });
1375
+ }
1376
+ function hasStateChanged(previous, next) {
1377
+ return JSON.stringify(previous) !== JSON.stringify(next);
1378
+ }
1379
+ function toErrorMessage(error) {
1380
+ return error instanceof Error ? error.message : String(error);
1381
+ }
1382
+
1383
+ export {
1384
+ isSecretType,
1385
+ isSecretSource,
1386
+ AisStore,
1387
+ PROTECT_TOOLS,
1388
+ isProtectTool,
1389
+ loadAutomationState,
1390
+ saveAutomationState,
1391
+ cloneAutomationState,
1392
+ expandHomePath3 as expandHomePath,
1393
+ loadConfig,
1394
+ saveConfig,
1395
+ syncProtectRuntime,
1396
+ refreshProtectRuntime,
1397
+ restoreProtectRuntime,
1398
+ resolveProtectedCommand
1399
+ };
1400
+ //# sourceMappingURL=chunk-WVTDLVUU.js.map