@phnx-labs/agents-cli 1.20.5 → 1.20.7
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/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/dist/commands/browser.js +31 -4
- package/dist/commands/computer-actions.d.ts +36 -0
- package/dist/commands/computer-actions.js +328 -0
- package/dist/commands/computer.js +74 -55
- package/dist/commands/defaults.d.ts +7 -0
- package/dist/commands/defaults.js +89 -0
- package/dist/commands/exec.js +24 -6
- package/dist/commands/inspect.d.ts +38 -7
- package/dist/commands/inspect.js +194 -24
- package/dist/commands/rules.js +3 -3
- package/dist/commands/secrets.js +46 -9
- package/dist/commands/sessions.js +9 -12
- package/dist/commands/setup.js +2 -2
- package/dist/commands/teams.js +108 -11
- package/dist/commands/view.d.ts +12 -1
- package/dist/commands/view.js +121 -38
- package/dist/index.js +61 -22
- package/dist/lib/agents.d.ts +10 -6
- package/dist/lib/agents.js +23 -14
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +84 -3
- package/dist/lib/daemon.js +4 -7
- package/dist/lib/exec.d.ts +9 -0
- package/dist/lib/exec.js +85 -9
- package/dist/lib/migrate.js +6 -4
- package/dist/lib/permissions.d.ts +23 -0
- package/dist/lib/permissions.js +89 -7
- package/dist/lib/platform/exec.d.ts +9 -0
- package/dist/lib/platform/exec.js +24 -0
- package/dist/lib/platform/index.d.ts +20 -0
- package/dist/lib/platform/index.js +20 -0
- package/dist/lib/platform/paths.d.ts +22 -0
- package/dist/lib/platform/paths.js +49 -0
- package/dist/lib/platform/process.d.ts +12 -0
- package/dist/lib/platform/process.js +22 -0
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/project-launch.d.ts +5 -0
- package/dist/lib/project-launch.js +37 -0
- package/dist/lib/pty-client.js +13 -5
- package/dist/lib/pty-server.d.ts +24 -1
- package/dist/lib/pty-server.js +109 -29
- package/dist/lib/resources/rules.js +1 -1
- package/dist/lib/resources/skills.js +1 -1
- package/dist/lib/resources.d.ts +2 -0
- package/dist/lib/resources.js +2 -1
- package/dist/lib/rotate.js +6 -18
- package/dist/lib/run-config.d.ts +9 -0
- package/dist/lib/run-config.js +35 -0
- package/dist/lib/run-defaults.d.ts +42 -0
- package/dist/lib/run-defaults.js +180 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/install-helper.d.ts +11 -3
- package/dist/lib/secrets/install-helper.js +48 -6
- package/dist/lib/secrets/linux.d.ts +12 -0
- package/dist/lib/secrets/linux.js +30 -16
- package/dist/lib/session/artifacts.js +8 -2
- package/dist/lib/shims.d.ts +9 -1
- package/dist/lib/shims.js +80 -3
- package/dist/lib/staleness/detectors/hooks.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/teams/agents.js +5 -7
- package/dist/lib/teams/api.d.ts +67 -0
- package/dist/lib/teams/api.js +78 -0
- package/dist/lib/types.d.ts +15 -6
- package/dist/lib/versions.js +4 -4
- package/package.json +5 -2
- package/scripts/postinstall.js +18 -1
|
@@ -144,6 +144,29 @@ export type GrokRule = {
|
|
|
144
144
|
tool: string;
|
|
145
145
|
pattern?: string;
|
|
146
146
|
};
|
|
147
|
+
export type KimiRule = {
|
|
148
|
+
decision: 'allow' | 'deny';
|
|
149
|
+
pattern: string;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Convert a canonical permission set to Kimi Code's `[permission].rules` format.
|
|
153
|
+
* Kimi (`~/.kimi-code/config.toml`) reads rules of the form
|
|
154
|
+
* [[permission.rules]]
|
|
155
|
+
* decision = "allow"
|
|
156
|
+
* pattern = "Bash(git status*)"
|
|
157
|
+
* Tool names are capitalized and the Bash arg-glob uses a trailing `*` (no
|
|
158
|
+
* Claude `:*` separator). Without this conversion the canonical strings match
|
|
159
|
+
* nothing in Kimi's engine and every tool call falls through to a prompt.
|
|
160
|
+
*
|
|
161
|
+
* Each `:*` Bash rule expands to TWO patterns (`cmd*` and `cmd*/**`) so the
|
|
162
|
+
* command auto-approves whether or not its arguments contain a slash — see
|
|
163
|
+
* `kimiBashPatterns` for why Kimi's picomatch matcher needs both.
|
|
164
|
+
*/
|
|
165
|
+
export declare function convertToKimiFormat(set: PermissionSet): {
|
|
166
|
+
permission: {
|
|
167
|
+
rules: KimiRule[];
|
|
168
|
+
};
|
|
169
|
+
};
|
|
147
170
|
/**
|
|
148
171
|
* Convert canonical permission set to OpenCode format.
|
|
149
172
|
* OpenCode uses: { permission: { bash: { "git *": "allow", "rm *": "deny" } } }
|
package/dist/lib/permissions.js
CHANGED
|
@@ -557,6 +557,94 @@ function canonicalToGrokRule(perm, action) {
|
|
|
557
557
|
}
|
|
558
558
|
return { action, tool, pattern };
|
|
559
559
|
}
|
|
560
|
+
/**
|
|
561
|
+
* Parse a canonical permission string preserving the tool's original casing.
|
|
562
|
+
* `parseCanonicalPattern` lowercases the tool name, which is fine for Grok
|
|
563
|
+
* (lowercase tool vocabulary) but wrong for Kimi, whose tool names are
|
|
564
|
+
* capitalized (`Bash`, `Read`, `Grep`). Bare tool names (no parens, e.g.
|
|
565
|
+
* `Read` or an MCP id like `mcp__server__tool`) return `pattern: null`.
|
|
566
|
+
*/
|
|
567
|
+
function parseCanonicalPreserveCase(perm) {
|
|
568
|
+
const m = perm.match(/^([\w-]+)\((.*)\)$/);
|
|
569
|
+
if (m)
|
|
570
|
+
return { tool: m[1], pattern: m[2] };
|
|
571
|
+
return { tool: perm, pattern: null };
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Translate a canonical Bash arg-glob (`cmd:*`) into the Kimi pattern(s) that
|
|
575
|
+
* actually match that command's invocations.
|
|
576
|
+
*
|
|
577
|
+
* Kimi matches Bash arg-globs with picomatch, where `*` does NOT cross `/` and a
|
|
578
|
+
* `**` only globstars when it is its own path segment (`*/**`, `**/`). A plain
|
|
579
|
+
* `cmd*` therefore matches `git status -s` but NOT `git push origin feat/x` or
|
|
580
|
+
* `cat dir/file` — any argument containing a slash falls through to a prompt
|
|
581
|
+
* (verified interactively against kimi 0.12.1). We emit TWO patterns so the
|
|
582
|
+
* command auto-approves whether its args contain a slash or not:
|
|
583
|
+
* - `cmd*` — no-slash args (and the bare command; `*` is zero-or-more).
|
|
584
|
+
* - `cmd*/**` — args with a path: `*` consumes up to the first `/`, then the
|
|
585
|
+
* bounded globstar crosses the remaining slashes.
|
|
586
|
+
* "git push:*" -> ["git push*", "git push*/**"].
|
|
587
|
+
*/
|
|
588
|
+
function kimiBashPatterns(pattern) {
|
|
589
|
+
if (pattern === '*' || pattern === '**')
|
|
590
|
+
return ['*'];
|
|
591
|
+
if (pattern.endsWith(':*')) {
|
|
592
|
+
const prefix = pattern.slice(0, -2);
|
|
593
|
+
return [`${prefix}*`, `${prefix}*/**`];
|
|
594
|
+
}
|
|
595
|
+
// Exact command (no `:*`, e.g. `env`, `pwd`, `true`) — no path args expected.
|
|
596
|
+
return [pattern];
|
|
597
|
+
}
|
|
598
|
+
function canonicalToKimiRules(perm, decision) {
|
|
599
|
+
if (BLANKET_BASH_FORMS.has(perm)) {
|
|
600
|
+
return [{ decision, pattern: 'Bash' }];
|
|
601
|
+
}
|
|
602
|
+
const { tool, pattern } = parseCanonicalPreserveCase(perm);
|
|
603
|
+
// Bare tool name (no parens) — name-only match. Covers `Read`, `Grep`, and
|
|
604
|
+
// MCP tool ids, which Kimi can only match by name anyway.
|
|
605
|
+
if (pattern === null) {
|
|
606
|
+
return [{ decision, pattern: tool }];
|
|
607
|
+
}
|
|
608
|
+
if (tool.toLowerCase() === 'bash') {
|
|
609
|
+
return kimiBashPatterns(pattern).map((p) => ({
|
|
610
|
+
decision,
|
|
611
|
+
pattern: p === '*' ? 'Bash' : `Bash(${p})`,
|
|
612
|
+
}));
|
|
613
|
+
}
|
|
614
|
+
// Non-Bash built-ins (Read/Write/Edit/Grep/Glob/WebFetch...) share Kimi's
|
|
615
|
+
// capitalized tool vocabulary, so pass the tool+pattern through. A `**`/`*`
|
|
616
|
+
// glob means "any" — collapse to a name-only rule.
|
|
617
|
+
if (pattern === '*' || pattern === '**') {
|
|
618
|
+
return [{ decision, pattern: tool }];
|
|
619
|
+
}
|
|
620
|
+
return [{ decision, pattern: `${tool}(${pattern})` }];
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Convert a canonical permission set to Kimi Code's `[permission].rules` format.
|
|
624
|
+
* Kimi (`~/.kimi-code/config.toml`) reads rules of the form
|
|
625
|
+
* [[permission.rules]]
|
|
626
|
+
* decision = "allow"
|
|
627
|
+
* pattern = "Bash(git status*)"
|
|
628
|
+
* Tool names are capitalized and the Bash arg-glob uses a trailing `*` (no
|
|
629
|
+
* Claude `:*` separator). Without this conversion the canonical strings match
|
|
630
|
+
* nothing in Kimi's engine and every tool call falls through to a prompt.
|
|
631
|
+
*
|
|
632
|
+
* Each `:*` Bash rule expands to TWO patterns (`cmd*` and `cmd*/**`) so the
|
|
633
|
+
* command auto-approves whether or not its arguments contain a slash — see
|
|
634
|
+
* `kimiBashPatterns` for why Kimi's picomatch matcher needs both.
|
|
635
|
+
*/
|
|
636
|
+
export function convertToKimiFormat(set) {
|
|
637
|
+
const rules = [];
|
|
638
|
+
for (const perm of set.allow) {
|
|
639
|
+
rules.push(...canonicalToKimiRules(perm, 'allow'));
|
|
640
|
+
}
|
|
641
|
+
if (set.deny) {
|
|
642
|
+
for (const perm of set.deny) {
|
|
643
|
+
rules.push(...canonicalToKimiRules(perm, 'deny'));
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return { permission: { rules } };
|
|
647
|
+
}
|
|
560
648
|
/**
|
|
561
649
|
* Convert canonical permission set to OpenCode format.
|
|
562
650
|
* OpenCode uses: { permission: { bash: { "git *": "allow", "rm *": "deny" } } }
|
|
@@ -1107,13 +1195,7 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
|
|
|
1107
1195
|
if (fs.existsSync(configPath)) {
|
|
1108
1196
|
config = TOML.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1109
1197
|
}
|
|
1110
|
-
const newRules =
|
|
1111
|
-
for (const allow of set.allow) {
|
|
1112
|
-
newRules.push({ decision: 'allow', pattern: allow });
|
|
1113
|
-
}
|
|
1114
|
-
for (const deny of set.deny || []) {
|
|
1115
|
-
newRules.push({ decision: 'deny', pattern: deny });
|
|
1116
|
-
}
|
|
1198
|
+
const newRules = convertToKimiFormat(set).permission.rules;
|
|
1117
1199
|
if (merge) {
|
|
1118
1200
|
const existingPermission = (typeof config.permission === 'object' && config.permission !== null && !Array.isArray(config.permission))
|
|
1119
1201
|
? config.permission
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** PATH-search command for the platform: `where` on Windows, else `which`. */
|
|
2
|
+
export declare function whichCommand(platform?: NodeJS.Platform): string;
|
|
3
|
+
/**
|
|
4
|
+
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
5
|
+
* `null` if not found. On Windows `where` can return several lines (one per
|
|
6
|
+
* PATHEXT match, e.g. `agents.cmd` and `agents.ps1`) — the first is the one the
|
|
7
|
+
* shell would actually run, matching `which` semantics on POSIX.
|
|
8
|
+
*/
|
|
9
|
+
export declare function findExecutable(name: string, platform?: NodeJS.Platform): string | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executable resolution, platform-aware.
|
|
3
|
+
*/
|
|
4
|
+
import { execFileSync } from 'child_process';
|
|
5
|
+
/** PATH-search command for the platform: `where` on Windows, else `which`. */
|
|
6
|
+
export function whichCommand(platform = process.platform) {
|
|
7
|
+
return platform === 'win32' ? 'where' : 'which';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
11
|
+
* `null` if not found. On Windows `where` can return several lines (one per
|
|
12
|
+
* PATHEXT match, e.g. `agents.cmd` and `agents.ps1`) — the first is the one the
|
|
13
|
+
* shell would actually run, matching `which` semantics on POSIX.
|
|
14
|
+
*/
|
|
15
|
+
export function findExecutable(name, platform = process.platform) {
|
|
16
|
+
try {
|
|
17
|
+
const out = execFileSync(whichCommand(platform), [name], { encoding: 'utf-8' });
|
|
18
|
+
const first = out.trim().split(/\r?\n/)[0]?.trim();
|
|
19
|
+
return first || null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform abstraction — the ONE place OS-divergent behavior is decided.
|
|
3
|
+
*
|
|
4
|
+
* Consumers express intent (`looksLikePath`, `findExecutable`, `isAlive`) instead
|
|
5
|
+
* of scattering `process.platform === 'win32'` checks. Each helper that has an
|
|
6
|
+
* observable branch accepts an explicit `platform` argument (defaulting to
|
|
7
|
+
* `process.platform`), so all three OSes are unit-testable on any host.
|
|
8
|
+
*
|
|
9
|
+
* Modules grow per concern as features land:
|
|
10
|
+
* paths — path classification + normalization
|
|
11
|
+
* exec — executable resolution
|
|
12
|
+
* process — process liveness / control
|
|
13
|
+
* (ipc + shell follow when their consumers migrate off inline branches.)
|
|
14
|
+
*/
|
|
15
|
+
export declare const IS_WINDOWS: boolean;
|
|
16
|
+
export declare const IS_MACOS: boolean;
|
|
17
|
+
export declare const IS_LINUX: boolean;
|
|
18
|
+
export * from './paths.js';
|
|
19
|
+
export * from './exec.js';
|
|
20
|
+
export * from './process.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform abstraction — the ONE place OS-divergent behavior is decided.
|
|
3
|
+
*
|
|
4
|
+
* Consumers express intent (`looksLikePath`, `findExecutable`, `isAlive`) instead
|
|
5
|
+
* of scattering `process.platform === 'win32'` checks. Each helper that has an
|
|
6
|
+
* observable branch accepts an explicit `platform` argument (defaulting to
|
|
7
|
+
* `process.platform`), so all three OSes are unit-testable on any host.
|
|
8
|
+
*
|
|
9
|
+
* Modules grow per concern as features land:
|
|
10
|
+
* paths — path classification + normalization
|
|
11
|
+
* exec — executable resolution
|
|
12
|
+
* process — process liveness / control
|
|
13
|
+
* (ipc + shell follow when their consumers migrate off inline branches.)
|
|
14
|
+
*/
|
|
15
|
+
export const IS_WINDOWS = process.platform === 'win32';
|
|
16
|
+
export const IS_MACOS = process.platform === 'darwin';
|
|
17
|
+
export const IS_LINUX = process.platform === 'linux';
|
|
18
|
+
export * from './paths.js';
|
|
19
|
+
export * from './exec.js';
|
|
20
|
+
export * from './process.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this positional argument look like a filesystem path (vs a search term)?
|
|
3
|
+
*
|
|
4
|
+
* POSIX markers (`.`, `./`, `../`, `/`, `~`) are recognized on every platform —
|
|
5
|
+
* identical to the long-standing behavior. Windows-only shapes (drive-letter
|
|
6
|
+
* `C:\…`, UNC `\\…`, backslash-relative `.\` / `..\`) are recognized ONLY on
|
|
7
|
+
* win32, so a literal `C:\repo` typed on macOS/Linux still resolves as a search
|
|
8
|
+
* term — i.e. no behavior change off Windows.
|
|
9
|
+
*/
|
|
10
|
+
export declare function looksLikePath(query: string, platform?: NodeJS.Platform): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Normalize a path for comparison/prefix-matching: backslashes folded to forward
|
|
13
|
+
* slashes and lowercased on Windows (its filesystem is case-insensitive). On
|
|
14
|
+
* POSIX the input is returned unchanged, so callers behave exactly as before.
|
|
15
|
+
*/
|
|
16
|
+
export declare function toComparablePath(p: string, platform?: NodeJS.Platform): string;
|
|
17
|
+
/**
|
|
18
|
+
* Canonical home directory. Use this instead of `process.env.HOME`, which is
|
|
19
|
+
* unset on Windows (where the home is `USERPROFILE`); `os.homedir()` resolves
|
|
20
|
+
* correctly on all three platforms.
|
|
21
|
+
*/
|
|
22
|
+
export declare function homeDir(): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path classification + normalization, platform-aware.
|
|
3
|
+
*/
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
/** Windows drive-letter absolute path: `C:\` or `C:/`. */
|
|
6
|
+
const WIN_DRIVE_RE = /^[a-zA-Z]:[\\/]/;
|
|
7
|
+
/**
|
|
8
|
+
* Does this positional argument look like a filesystem path (vs a search term)?
|
|
9
|
+
*
|
|
10
|
+
* POSIX markers (`.`, `./`, `../`, `/`, `~`) are recognized on every platform —
|
|
11
|
+
* identical to the long-standing behavior. Windows-only shapes (drive-letter
|
|
12
|
+
* `C:\…`, UNC `\\…`, backslash-relative `.\` / `..\`) are recognized ONLY on
|
|
13
|
+
* win32, so a literal `C:\repo` typed on macOS/Linux still resolves as a search
|
|
14
|
+
* term — i.e. no behavior change off Windows.
|
|
15
|
+
*/
|
|
16
|
+
export function looksLikePath(query, platform = process.platform) {
|
|
17
|
+
if (query === '.' ||
|
|
18
|
+
query.startsWith('./') ||
|
|
19
|
+
query.startsWith('../') ||
|
|
20
|
+
query.startsWith('/') ||
|
|
21
|
+
query.startsWith('~')) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
if (platform === 'win32') {
|
|
25
|
+
return (WIN_DRIVE_RE.test(query) ||
|
|
26
|
+
query.startsWith('\\\\') ||
|
|
27
|
+
query.startsWith('.\\') ||
|
|
28
|
+
query.startsWith('..\\'));
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Normalize a path for comparison/prefix-matching: backslashes folded to forward
|
|
34
|
+
* slashes and lowercased on Windows (its filesystem is case-insensitive). On
|
|
35
|
+
* POSIX the input is returned unchanged, so callers behave exactly as before.
|
|
36
|
+
*/
|
|
37
|
+
export function toComparablePath(p, platform = process.platform) {
|
|
38
|
+
if (platform === 'win32')
|
|
39
|
+
return p.replace(/\\/g, '/').toLowerCase();
|
|
40
|
+
return p;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Canonical home directory. Use this instead of `process.env.HOME`, which is
|
|
44
|
+
* unset on Windows (where the home is `USERPROFILE`); `os.homedir()` resolves
|
|
45
|
+
* correctly on all three platforms.
|
|
46
|
+
*/
|
|
47
|
+
export function homeDir() {
|
|
48
|
+
return os.homedir();
|
|
49
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process liveness / control, platform-aware.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Is a process with this PID currently alive?
|
|
6
|
+
*
|
|
7
|
+
* Uses the signal-0 probe, which is cross-platform in Node (Windows included —
|
|
8
|
+
* it maps to OpenProcess). Returns false on any error (no such process, or no
|
|
9
|
+
* permission to signal it), matching the long-standing call sites that treat a
|
|
10
|
+
* throw from `process.kill(pid, 0)` as "not running".
|
|
11
|
+
*/
|
|
12
|
+
export declare function isAlive(pid: number): boolean;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process liveness / control, platform-aware.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Is a process with this PID currently alive?
|
|
6
|
+
*
|
|
7
|
+
* Uses the signal-0 probe, which is cross-platform in Node (Windows included —
|
|
8
|
+
* it maps to OpenProcess). Returns false on any error (no such process, or no
|
|
9
|
+
* permission to signal it), matching the long-standing call sites that treat a
|
|
10
|
+
* throw from `process.kill(pid, 0)` as "not running".
|
|
11
|
+
*/
|
|
12
|
+
export function isAlive(pid) {
|
|
13
|
+
if (!pid || pid <= 0)
|
|
14
|
+
return false;
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
* the catalog name and on-disk layout are derived, never hard-coded per call.
|
|
27
27
|
*/
|
|
28
28
|
import * as fs from 'fs';
|
|
29
|
+
import { agentConfigDirName } from './agents.js';
|
|
29
30
|
import * as path from 'path';
|
|
30
31
|
import { getPluginsDir, getEnabledExtraRepos, getProjectPluginsDir } from './state.js';
|
|
31
|
-
import { agentConfigDirName } from './agents.js';
|
|
32
32
|
/**
|
|
33
33
|
* Canonical name for the user-repo marketplace (~/.agents/plugins/). Kept as an
|
|
34
34
|
* exported constant for callers that operate on the user repo directly and for
|
|
@@ -60,6 +60,11 @@ export interface LaunchSyncResult {
|
|
|
60
60
|
/**
|
|
61
61
|
* Run the launch-time project compile. Safe to call on every agent launch:
|
|
62
62
|
* each step is idempotent and skips when its inputs are missing.
|
|
63
|
+
*
|
|
64
|
+
* After a successful run, touches the shim-side skip-fast sentinel at
|
|
65
|
+
* `~/.agents/.cache/launch-sync/<agent>@<version>@<projectslug>` so the next
|
|
66
|
+
* shim invocation can skip the node spawn entirely when no source dir is
|
|
67
|
+
* newer than the sentinel (shim schema v17+).
|
|
63
68
|
*/
|
|
64
69
|
export declare function runLaunchSync(opts: LaunchSyncOptions): LaunchSyncResult;
|
|
65
70
|
export { pluginInstallDir };
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
*/
|
|
43
43
|
import * as crypto from 'crypto';
|
|
44
44
|
import * as fs from 'fs';
|
|
45
|
+
import * as os from 'os';
|
|
45
46
|
import * as path from 'path';
|
|
46
47
|
import { supports } from './capabilities.js';
|
|
47
48
|
import { getEnabledExtraRepos, getExtraPluginsDir, getPluginsDir, getProjectAgentsDir, getProjectPluginsDir, getSystemPluginsDir, } from './state.js';
|
|
@@ -52,6 +53,11 @@ import { MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, SYSTEM_MARKETPLACE_NAME, ad
|
|
|
52
53
|
/**
|
|
53
54
|
* Run the launch-time project compile. Safe to call on every agent launch:
|
|
54
55
|
* each step is idempotent and skips when its inputs are missing.
|
|
56
|
+
*
|
|
57
|
+
* After a successful run, touches the shim-side skip-fast sentinel at
|
|
58
|
+
* `~/.agents/.cache/launch-sync/<agent>@<version>@<projectslug>` so the next
|
|
59
|
+
* shim invocation can skip the node spawn entirely when no source dir is
|
|
60
|
+
* newer than the sentinel (shim schema v17+).
|
|
55
61
|
*/
|
|
56
62
|
export function runLaunchSync(opts) {
|
|
57
63
|
const result = {
|
|
@@ -74,8 +80,39 @@ export function runLaunchSync(opts) {
|
|
|
74
80
|
result.workspaceSkipped = mirror.skipped;
|
|
75
81
|
// Step 3: scoped plugin marketplaces
|
|
76
82
|
result.marketplaces = synthesizeScopedMarketplaces(opts.agent, opts.version, opts.cwd);
|
|
83
|
+
// Touch the shim's skip-fast sentinel. Best-effort — if this fails the
|
|
84
|
+
// shim just won't skip on the next launch, which is correct fallback.
|
|
85
|
+
touchLaunchSentinel(opts.agent, opts.version, opts.cwd);
|
|
77
86
|
return result;
|
|
78
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Path of the shim's skip-fast sentinel for this (agent, version, cwd) tuple.
|
|
90
|
+
* Must match the SHIM-SIDE format in src/lib/shims.ts (PROJECT_SLUG derivation):
|
|
91
|
+
* slug = PWD with `/` → `_` and ` ` → `_`
|
|
92
|
+
*
|
|
93
|
+
* Cache leak note: this dir accumulates one zero-byte file per
|
|
94
|
+
* (agent, version, project) tuple ever launched. Disk impact is negligible
|
|
95
|
+
* (inodes only). A periodic GC belongs in `agents prune` — follow-up.
|
|
96
|
+
*/
|
|
97
|
+
function launchSentinelPath(agent, version, cwd) {
|
|
98
|
+
const slug = cwd.replace(/\//g, '_').replace(/ /g, '_');
|
|
99
|
+
// Prefer $HOME (respects test overrides + matches bash's $HOME expansion in
|
|
100
|
+
// the shim), fall back to os.homedir() so the lookup never resolves to '/'
|
|
101
|
+
// if HOME is somehow unset.
|
|
102
|
+
const home = process.env.HOME || os.homedir();
|
|
103
|
+
return path.join(home, '.agents', '.cache', 'launch-sync', `${agent}@${version}@${slug}`);
|
|
104
|
+
}
|
|
105
|
+
function touchLaunchSentinel(agent, version, cwd) {
|
|
106
|
+
try {
|
|
107
|
+
const sentinel = launchSentinelPath(agent, version, cwd);
|
|
108
|
+
fs.mkdirSync(path.dirname(sentinel), { recursive: true });
|
|
109
|
+
// Empty content — purely an mtime carrier for the shim's `[ -nt ]` compare.
|
|
110
|
+
fs.writeFileSync(sentinel, '');
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// best-effort
|
|
114
|
+
}
|
|
115
|
+
}
|
|
79
116
|
const CLAUDE_MIRROR_PLANS = [
|
|
80
117
|
{ srcSubdir: 'subagents', destSubdir: 'agents', entriesAreDirs: false },
|
|
81
118
|
{ srcSubdir: 'commands', destSubdir: 'commands', entriesAreDirs: false },
|
package/dist/lib/pty-client.js
CHANGED
|
@@ -12,6 +12,7 @@ import * as path from 'path';
|
|
|
12
12
|
import { getSocketPath, getPtyLogPath, isPtyServerRunning } from './pty-server.js';
|
|
13
13
|
const CONNECT_TIMEOUT_MS = 5000;
|
|
14
14
|
const RESPONSE_TIMEOUT_MS = 30000;
|
|
15
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
15
16
|
/**
|
|
16
17
|
* Send a request to the PTY server and return the response.
|
|
17
18
|
* Auto-starts the server if not running.
|
|
@@ -41,11 +42,13 @@ async function ensureServer() {
|
|
|
41
42
|
});
|
|
42
43
|
child.unref();
|
|
43
44
|
fs.closeSync(logFd);
|
|
44
|
-
// Wait for
|
|
45
|
+
// Wait for the server to become reachable. On Unix the socket file appearing is
|
|
46
|
+
// a cheap readiness signal; on Windows the named pipe is not a filesystem object
|
|
47
|
+
// (fs.existsSync always returns false), so we just attempt the ping directly.
|
|
45
48
|
const socketPath = getSocketPath();
|
|
46
49
|
const deadline = Date.now() + 5000;
|
|
47
50
|
while (Date.now() < deadline) {
|
|
48
|
-
if (fs.existsSync(socketPath)) {
|
|
51
|
+
if (IS_WINDOWS || fs.existsSync(socketPath)) {
|
|
49
52
|
// Verify we can connect
|
|
50
53
|
try {
|
|
51
54
|
await sendRequest({ action: 'ping' });
|
|
@@ -70,9 +73,11 @@ function getServerSpawnArgs() {
|
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
catch { }
|
|
73
|
-
// Fallback: use the globally installed agents command
|
|
76
|
+
// Fallback: use the globally installed agents command. `which` is Unix-only;
|
|
77
|
+
// Windows uses `where`, which can return multiple lines — take the first.
|
|
74
78
|
try {
|
|
75
|
-
const
|
|
79
|
+
const lookup = IS_WINDOWS ? 'where agents' : 'which agents';
|
|
80
|
+
const agentsBin = execSync(lookup, { encoding: 'utf-8' }).split(/\r?\n/)[0].trim();
|
|
76
81
|
if (agentsBin) {
|
|
77
82
|
return { bin: agentsBin, args: ['pty', '_server'] };
|
|
78
83
|
}
|
|
@@ -86,7 +91,10 @@ function getServerSpawnArgs() {
|
|
|
86
91
|
function sendRequest(req) {
|
|
87
92
|
return new Promise((resolve, reject) => {
|
|
88
93
|
const socketPath = getSocketPath();
|
|
89
|
-
|
|
94
|
+
// On Unix a missing socket file means the server isn't up — fail fast with a
|
|
95
|
+
// clear message. On Windows the named pipe isn't a filesystem object, so we
|
|
96
|
+
// skip the probe and let createConnection surface ENOENT/connection errors.
|
|
97
|
+
if (!IS_WINDOWS && !fs.existsSync(socketPath)) {
|
|
90
98
|
reject(new Error('PTY server socket not found. Is the server running?'));
|
|
91
99
|
return;
|
|
92
100
|
}
|
package/dist/lib/pty-server.d.ts
CHANGED
|
@@ -18,7 +18,30 @@
|
|
|
18
18
|
* Returns null on any error so callers can skip the guard rather than crash.
|
|
19
19
|
*/
|
|
20
20
|
export declare function captureProcessStartTime(pid: number): string | null;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Wrap a user command so a `__SENTINEL__:<exit>` line is printed after it
|
|
23
|
+
* finishes — that line drives completion detection in the exec/read flow.
|
|
24
|
+
* The separator and exit-code variable are shell-family specific:
|
|
25
|
+
* POSIX sh/zsh/bash : `cmd; echo "S:$?"`
|
|
26
|
+
* PowerShell : `cmd; echo "S:$LASTEXITCODE"`
|
|
27
|
+
* cmd.exe : `cmd & echo S:%errorlevel%` (`&` always runs the echo)
|
|
28
|
+
* Only the completion marker matters; the numeric exit code is informational
|
|
29
|
+
* (the authoritative code comes from node-pty's onExit).
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildSentinelCommand(shell: string, command: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the IPC endpoint for a given platform + PTY scratch dir. Pure so both
|
|
34
|
+
* branches are testable without stubbing process.platform.
|
|
35
|
+
*
|
|
36
|
+
* Unix: an AF_UNIX socket file inside the scratch dir.
|
|
37
|
+
* Windows: a named pipe (`\\.\pipe\…`). Named pipes are NOT filesystem objects,
|
|
38
|
+
* so the name is derived from a hash of the (per-user) scratch dir to keep it
|
|
39
|
+
* stable across invocations and isolated per user — and callers must never probe
|
|
40
|
+
* it with fs.existsSync (it always reports false). Both forms are accepted by
|
|
41
|
+
* net.createServer/createConnection.
|
|
42
|
+
*/
|
|
43
|
+
export declare function derivePtyEndpoint(platform: NodeJS.Platform, ptyDir: string): string;
|
|
44
|
+
/** Get the IPC endpoint the PTY server listens on / clients connect to. */
|
|
22
45
|
export declare function getSocketPath(): string;
|
|
23
46
|
/** Get the path to the PTY server PID file. */
|
|
24
47
|
export declare function getPtyPidPath(): string;
|