pi-maestro-teammate 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/bin/pi-teammate-outbox.mjs +12 -0
- package/package.json +19 -6
- package/src/backends/acp-cli-catalog.ts +6 -0
- package/src/backends/acp-cli.ts +25 -8
- package/src/cli-tools/cli-tools-config.ts +117 -40
- package/src/cli-tools/local-acp.ts +8 -4
- package/src/completion-outbox/cli.ts +123 -0
- package/src/completion-outbox/coordinator.ts +7 -12
- package/src/completion-outbox/file-store.ts +635 -169
- package/src/completion-outbox/types.ts +2 -0
- package/src/extension/herdr-window-provider.ts +794 -0
- package/src/extension/index.ts +1473 -337
- package/src/extension/mailbox/file-store.ts +29 -17
- package/src/extension/mailbox/host.ts +29 -10
- package/src/extension/mailbox/rollout.ts +15 -5
- package/src/extension/mailbox/router.ts +79 -33
- package/src/extension/mailbox/service.ts +4 -0
- package/src/extension/mailbox/types.ts +11 -2
- package/src/extension/monitor-window-lifecycle.ts +278 -0
- package/src/extension/monitor-window-state.ts +514 -0
- package/src/extension/monitor.ts +616 -15
- package/src/extension/remote-window-monitor.ts +357 -0
- package/src/extension/remote-window-session.ts +50 -0
- package/src/extension/runtime-actor-host.ts +113 -2
- package/src/extension/schemas.ts +125 -10
- package/src/extension/session-endpoints.ts +168 -20
- package/src/extension/task-delegation.ts +2 -2
- package/src/extension/teammate-core.ts +14 -6
- package/src/extension/teammate-helpers.ts +19 -6
- package/src/extension/teammate-proxy.ts +40 -3
- package/src/extension/tool-error-signal.ts +42 -0
- package/src/extension/workspace-peers.ts +8 -2848
- package/src/extension/workspace-turns-observation.ts +8 -5
- package/src/models/cli-add.ts +8 -2
- package/src/models/model-routing.ts +92 -0
- package/src/public/v1/index.ts +3 -0
- package/src/public/v1/model-routing.ts +6 -0
- package/src/public/v1/monitor-window-state.ts +525 -0
- package/src/public/v1/session-history.ts +2 -0
- package/src/public/v1/ssh-hosts.ts +209 -0
- package/src/public/v2/runtime-broker.ts +6 -0
- package/src/public/v2/runtime.ts +3 -0
- package/src/remote/child-security.ts +30 -4
- package/src/remote/cli.ts +3 -1
- package/src/remote/config.ts +131 -17
- package/src/remote/driver.ts +15 -0
- package/src/remote/process-tree.ts +12 -28
- package/src/remote/protocol.ts +164 -1
- package/src/remote/server.ts +93 -1
- package/src/remote/ssh-exec.ts +17 -1
- package/src/remote/ssh.ts +162 -17
- package/src/remote/types.ts +29 -2
- package/src/remote/window-protocol.ts +501 -0
- package/src/remote/window-service.ts +566 -0
- package/src/runs/execution-infra.ts +110 -11
- package/src/runs/execution.ts +17 -12
- package/src/runs/pi-subprocess-attempt.ts +2960 -2755
- package/src/runtime-broker/client.ts +63 -21
- package/src/runtime-broker/mailbox-commit.ts +4 -3
- package/src/runtime-broker/session-commit.ts +121 -0
- package/src/runtime-broker/sqlite-store.ts +52 -2
- package/src/runtime-broker/transport.ts +2 -1
- package/src/runtime-v2/session-broker-read-model.ts +101 -0
- package/src/runtime-v2/session-domain.ts +397 -0
- package/src/runtime-v2/session-rollout.ts +90 -0
- package/src/sessions/session-core.ts +155 -22
- package/src/sessions/workspace-peer-core.ts +2993 -0
- package/src/shared/agent-status.ts +4 -1
- package/src/shared/turn-ledger.ts +30 -6
- package/src/shared/types.ts +7 -4
- package/src/transcript/session-history.ts +758 -0
- package/src/tui/attach-overlay.ts +23 -14
- package/src/tui/connection-wizards.ts +177 -16
- package/src/tui/locale-catalog-model.ts +36 -0
- package/src/tui/model-mapping-overlay.ts +60 -3
- package/src/tui/remote-config-pane.ts +114 -16
- package/src/tui/render.ts +504 -25
- package/src/tui/session-send-overlay.ts +6 -1
- package/types/backends/acp-cli.d.ts +1 -0
- package/types/cli-tools/cli-tools-config.d.ts +8 -3
- package/types/completion-outbox/cli.d.ts +1 -0
- package/types/completion-outbox/file-store.d.ts +21 -5
- package/types/completion-outbox/types.d.ts +2 -0
- package/types/extension/herdr-window-provider.d.ts +165 -0
- package/types/extension/mailbox/host.d.ts +2 -1
- package/types/extension/mailbox/rollout.d.ts +9 -3
- package/types/extension/mailbox/router.d.ts +4 -0
- package/types/extension/mailbox/service.d.ts +4 -0
- package/types/extension/mailbox/types.d.ts +9 -2
- package/types/extension/monitor-window-lifecycle.d.ts +96 -0
- package/types/extension/monitor-window-state.d.ts +45 -0
- package/types/extension/monitor.d.ts +105 -0
- package/types/extension/remote-window-monitor.d.ts +52 -0
- package/types/extension/remote-window-session.d.ts +12 -0
- package/types/extension/runtime-actor-host.d.ts +3 -0
- package/types/extension/schemas.d.ts +24 -12
- package/types/extension/session-endpoints.d.ts +27 -3
- package/types/extension/teammate-core.d.ts +2 -2
- package/types/extension/teammate-helpers.d.ts +2 -0
- package/types/extension/teammate-proxy.d.ts +3 -3
- package/types/extension/tool-error-signal.d.ts +7 -0
- package/types/extension/workspace-peers.d.ts +4 -558
- package/types/models/model-routing.d.ts +17 -0
- package/types/public/v1/index.d.ts +3 -0
- package/types/public/v1/model-routing.d.ts +1 -0
- package/types/public/v1/monitor-window-state.d.ts +183 -0
- package/types/public/v1/session-history.d.ts +2 -0
- package/types/public/v1/ssh-hosts.d.ts +24 -0
- package/types/public/v2/runtime-broker.d.ts +1 -0
- package/types/public/v2/runtime.d.ts +3 -0
- package/types/remote/child-security.d.ts +1 -0
- package/types/remote/config.d.ts +12 -4
- package/types/remote/driver.d.ts +8 -2
- package/types/remote/process-tree.d.ts +4 -4
- package/types/remote/protocol.d.ts +40 -4
- package/types/remote/server.d.ts +9 -1
- package/types/remote/ssh-exec.d.ts +3 -0
- package/types/remote/ssh.d.ts +9 -1
- package/types/remote/types.d.ts +22 -2
- package/types/remote/window-protocol.d.ts +127 -0
- package/types/remote/window-service.d.ts +21 -0
- package/types/runs/execution-infra.d.ts +20 -0
- package/types/runs/pi-subprocess-attempt.d.ts +1 -1
- package/types/runtime-broker/client.d.ts +2 -0
- package/types/runtime-broker/mailbox-commit.d.ts +2 -1
- package/types/runtime-broker/session-commit.d.ts +33 -0
- package/types/runtime-broker/transport.d.ts +2 -2
- package/types/runtime-v2/session-broker-read-model.d.ts +16 -0
- package/types/runtime-v2/session-domain.d.ts +84 -0
- package/types/runtime-v2/session-rollout.d.ts +24 -0
- package/types/sessions/session-core.d.ts +48 -8
- package/types/sessions/workspace-peer-core.d.ts +603 -0
- package/types/shared/types.d.ts +5 -3
- package/types/transcript/session-history.d.ts +170 -0
- package/types/tui/attach-overlay.d.ts +2 -0
- package/types/tui/connection-wizards.d.ts +13 -2
- package/types/tui/locale-catalog-model.d.ts +36 -0
- package/types/tui/model-mapping-overlay.d.ts +2 -0
- package/types/tui/remote-config-pane.d.ts +20 -0
- package/types/tui/render.d.ts +32 -1
package/README.md
CHANGED
|
@@ -187,7 +187,17 @@ project .pi/agents > project .agents > ~/.agents > legacy user directory > bundl
|
|
|
187
187
|
|
|
188
188
|
## Model Routing Profiles
|
|
189
189
|
|
|
190
|
-
Configure task-type defaults with `Alt+M` or `/teammate-models`. The Control Center's **Profiles** tab manages named routing Profiles shared by every project. Its **Routing** tab edits the active global Profile and combines built-in task types, discovered agent types, and types already present in that Profile.
|
|
190
|
+
Configure task-type defaults with `Alt+M` or `/teammate-models`. The Control Center's **Profiles** tab is the saved-template system: it manages multiple named routing Profiles shared by every project. Its **Routing** tab edits the active global Profile and combines built-in task types, discovered agent types, and types already present in that Profile.
|
|
191
|
+
|
|
192
|
+
Switch the current project's template directly by stable Profile ID or display name. With no argument, the command opens the Control Center on **Profiles**:
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
/teammate-model fast-lane
|
|
196
|
+
/teammate-model Fast Lane
|
|
197
|
+
/teammate-model
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Command completion lists every saved template and marks the project-active and global-default entries. If display names are duplicated, use the stable ID. Direct switching disables but preserves project overrides, matching the Profiles UI.
|
|
191
201
|
|
|
192
202
|
The **Roles** tab lists discovered roles and their effective durable route. Role-scoped settings can be edited through the unified Settings surface as `role.<role-name>.model`, `role.<role-name>.fallbacks`, and `role.<role-name>.thinking`. These settings are stored in the same global Profile or project override file; package updates never rewrite role definitions or user routing data.
|
|
193
203
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createJiti } from "jiti";
|
|
3
|
+
|
|
4
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
5
|
+
const { main } = await jiti.import("../src/completion-outbox/cli.ts");
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
9
|
+
} catch (error) {
|
|
10
|
+
process.stderr.write(`pi-teammate-outbox: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-maestro-teammate",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Pi extension — teammate agent dispatch with DAG task graphs, RPC messaging, and compact TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"bin": {
|
|
10
10
|
"pi-teammate-broker": "./bin/pi-teammate-broker.mjs",
|
|
11
11
|
"pi-teammate-models": "./bin/pi-teammate-models.mjs",
|
|
12
|
+
"pi-teammate-outbox": "./bin/pi-teammate-outbox.mjs",
|
|
12
13
|
"pi-teammate-remote": "./bin/pi-teammate-remote.mjs"
|
|
13
14
|
},
|
|
14
15
|
"keywords": [
|
|
@@ -19,7 +20,7 @@
|
|
|
19
20
|
"agents"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
22
|
-
"test": "node --experimental-transform-types --import ./test/setup.ts --test --test-concurrency=
|
|
23
|
+
"test": "node --experimental-transform-types --import ./test/setup.ts --test --test-concurrency=2 \"test/*.test.ts\"",
|
|
23
24
|
"typecheck": "tsc -p tsconfig.build.json",
|
|
24
25
|
"build:declarations": "tsc -p tsconfig.declarations.json",
|
|
25
26
|
"check:declarations": "node scripts/check-declarations.mjs",
|
|
@@ -72,6 +73,10 @@
|
|
|
72
73
|
"types": "./types/public/v1/model-routing.d.ts",
|
|
73
74
|
"default": "./src/public/v1/model-routing.ts"
|
|
74
75
|
},
|
|
76
|
+
"./v1/monitor-window-state": {
|
|
77
|
+
"types": "./types/public/v1/monitor-window-state.d.ts",
|
|
78
|
+
"default": "./src/public/v1/monitor-window-state.ts"
|
|
79
|
+
},
|
|
75
80
|
"./v1/cli-tools": {
|
|
76
81
|
"types": "./types/public/v1/cli-tools.d.ts",
|
|
77
82
|
"default": "./src/public/v1/cli-tools.ts"
|
|
@@ -100,6 +105,10 @@
|
|
|
100
105
|
"types": "./types/public/v1/retry.d.ts",
|
|
101
106
|
"default": "./src/public/v1/retry.ts"
|
|
102
107
|
},
|
|
108
|
+
"./v1/session-history": {
|
|
109
|
+
"types": "./types/public/v1/session-history.d.ts",
|
|
110
|
+
"default": "./src/public/v1/session-history.ts"
|
|
111
|
+
},
|
|
103
112
|
"./v1/scheduler": {
|
|
104
113
|
"types": "./types/public/v1/scheduler.d.ts",
|
|
105
114
|
"default": "./src/public/v1/scheduler.ts"
|
|
@@ -112,6 +121,10 @@
|
|
|
112
121
|
"types": "./types/public/v1/supervision.d.ts",
|
|
113
122
|
"default": "./src/public/v1/supervision.ts"
|
|
114
123
|
},
|
|
124
|
+
"./v1/ssh-hosts": {
|
|
125
|
+
"types": "./types/public/v1/ssh-hosts.d.ts",
|
|
126
|
+
"default": "./src/public/v1/ssh-hosts.ts"
|
|
127
|
+
},
|
|
115
128
|
"./v1/workspace-projections": {
|
|
116
129
|
"types": "./types/public/v1/workspace-projections.d.ts",
|
|
117
130
|
"default": "./src/public/v1/workspace-projections.ts"
|
|
@@ -192,11 +205,11 @@
|
|
|
192
205
|
"pi-maestro-settings-core"
|
|
193
206
|
],
|
|
194
207
|
"devDependencies": {
|
|
195
|
-
"@earendil-works/pi-agent-core": "0.
|
|
208
|
+
"@earendil-works/pi-agent-core": "0.84.4",
|
|
196
209
|
"@types/cross-spawn": "6.0.6",
|
|
197
|
-
"@earendil-works/pi-ai": "0.
|
|
198
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
199
|
-
"@earendil-works/pi-tui": "0.
|
|
210
|
+
"@earendil-works/pi-ai": "0.84.4",
|
|
211
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
212
|
+
"@earendil-works/pi-tui": "0.84.4",
|
|
200
213
|
"@types/ssh2": "1.15.5",
|
|
201
214
|
"typebox": "1.3.7"
|
|
202
215
|
},
|
|
@@ -43,6 +43,9 @@ export const ACP_CLI_SETTINGS_CATALOGS: TranslationCatalogs = {
|
|
|
43
43
|
"acpCli.mode": "Launch mode",
|
|
44
44
|
"acpCli.mode.local": "Local process",
|
|
45
45
|
"acpCli.mode.ssh": "Remote over SSH",
|
|
46
|
+
"acpCli.sshHostRef": "/ssh host reference",
|
|
47
|
+
"acpCli.sshHostRef.description":
|
|
48
|
+
"Host id from the unlocked encrypted /ssh manager. Use it instead of every embedded SSH connection field; the host is resolved again for each run.",
|
|
46
49
|
"acpCli.host": "SSH host",
|
|
47
50
|
"acpCli.user": "SSH user",
|
|
48
51
|
"acpCli.port": "SSH port",
|
|
@@ -92,6 +95,9 @@ export const ACP_CLI_SETTINGS_CATALOGS: TranslationCatalogs = {
|
|
|
92
95
|
"acpCli.mode": "启动方式",
|
|
93
96
|
"acpCli.mode.local": "本地进程",
|
|
94
97
|
"acpCli.mode.ssh": "经 SSH 远程",
|
|
98
|
+
"acpCli.sshHostRef": "/ssh 主机引用",
|
|
99
|
+
"acpCli.sshHostRef.description":
|
|
100
|
+
"已解锁加密 /ssh 管理器中的主机 id。它必须替代全部内嵌 SSH 连接字段,并在每次运行时重新解析。",
|
|
95
101
|
"acpCli.host": "SSH 主机",
|
|
96
102
|
"acpCli.user": "SSH 用户",
|
|
97
103
|
"acpCli.port": "SSH 端口",
|
package/src/backends/acp-cli.ts
CHANGED
|
@@ -52,6 +52,7 @@ import type {
|
|
|
52
52
|
import {
|
|
53
53
|
cliToolArgv,
|
|
54
54
|
probeCliToolCommand,
|
|
55
|
+
sshHostConfigIssue,
|
|
55
56
|
type CliToolConfig,
|
|
56
57
|
} from "../cli-tools/cli-tools-config.ts";
|
|
57
58
|
import {
|
|
@@ -282,9 +283,18 @@ export const ACP_CLI_CONFIG_FIELDS: readonly BackendConfigField[] = [
|
|
|
282
283
|
labelKey: "acpCli.mode",
|
|
283
284
|
default: "local",
|
|
284
285
|
},
|
|
286
|
+
{
|
|
287
|
+
key: "sshHostRef",
|
|
288
|
+
kind: "text",
|
|
289
|
+
labelKey: "acpCli.sshHostRef",
|
|
290
|
+
descriptionKey: "acpCli.sshHostRef.description",
|
|
291
|
+
},
|
|
285
292
|
{ key: "host", kind: "text", labelKey: "acpCli.host" },
|
|
286
293
|
{ key: "user", kind: "text", labelKey: "acpCli.user" },
|
|
287
|
-
|
|
294
|
+
// Applied only to embedded SSH below. Leaving this declaration without a
|
|
295
|
+
// generic default lets validation distinguish an explicit override from a
|
|
296
|
+
// reference that must take its port exclusively from `/ssh`.
|
|
297
|
+
{ key: "port", kind: "integer", labelKey: "acpCli.port" },
|
|
288
298
|
{ key: "hostKeySha256", kind: "text", labelKey: "acpCli.hostKeySha256" },
|
|
289
299
|
{ key: "identityFile", kind: "path", labelKey: "acpCli.identityFile" },
|
|
290
300
|
{
|
|
@@ -420,6 +430,7 @@ async function installedLaunchConfig(
|
|
|
420
430
|
function launchConfigOf(config: Record<string, ConfigValue>): CliToolConfig {
|
|
421
431
|
const command = text(config, "command");
|
|
422
432
|
const cwd = text(config, "cwd");
|
|
433
|
+
const sshHostRef = text(config, "sshHostRef");
|
|
423
434
|
const host = text(config, "host");
|
|
424
435
|
const user = text(config, "user");
|
|
425
436
|
const port = count(config, "port");
|
|
@@ -432,6 +443,7 @@ function launchConfigOf(config: Record<string, ConfigValue>): CliToolConfig {
|
|
|
432
443
|
args: list(config, "args"),
|
|
433
444
|
...(cwd === undefined ? {} : { cwd }),
|
|
434
445
|
env: list(config, "env"),
|
|
446
|
+
...(sshHostRef === undefined ? {} : { sshHostRef }),
|
|
435
447
|
...(host === undefined ? {} : { host }),
|
|
436
448
|
...(user === undefined ? {} : { user }),
|
|
437
449
|
...(port === undefined ? {} : { port }),
|
|
@@ -605,14 +617,18 @@ export function createAcpCliBackend(
|
|
|
605
617
|
+ "parent process forwards by name; a name=value entry puts the value in the registration document",
|
|
606
618
|
);
|
|
607
619
|
}
|
|
608
|
-
// Checked at load rather than at launch:
|
|
609
|
-
//
|
|
610
|
-
//
|
|
620
|
+
// Checked at load rather than at launch: one SSH source must be complete
|
|
621
|
+
// and authoritative. A reference cannot be paired with even a seemingly
|
|
622
|
+
// harmless port override, because that would recreate two sources of
|
|
623
|
+
// truth for the connection.
|
|
611
624
|
if (mode === "ssh") {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
625
|
+
const issue = sshHostConfigIssue(launchConfigOf(values));
|
|
626
|
+
if (issue !== undefined) errors.push(issue);
|
|
627
|
+
else if ((text(values, "sshHostRef") ?? "").trim().length === 0 && count(values, "port") === undefined) {
|
|
628
|
+
values.port = 22;
|
|
615
629
|
}
|
|
630
|
+
} else if ((text(values, "sshHostRef") ?? "").trim().length > 0) {
|
|
631
|
+
errors.push('"sshHostRef" requires "mode" to be "ssh"');
|
|
616
632
|
}
|
|
617
633
|
return { values, errors };
|
|
618
634
|
},
|
|
@@ -638,7 +654,7 @@ export function createAcpCliBackend(
|
|
|
638
654
|
// beats returning the local machine's answer for a remote target.
|
|
639
655
|
throw new Error(
|
|
640
656
|
`teammate backend "acp-cli" cannot list "${field}" for an "ssh" registration: `
|
|
641
|
-
+ "
|
|
657
|
+
+ "whether embedded or selected by sshHostRef, the target host's catalogue is not reachable by the local probe",
|
|
642
658
|
);
|
|
643
659
|
}
|
|
644
660
|
const route = routeOf(config);
|
|
@@ -796,6 +812,7 @@ export function createAcpCliBackend(
|
|
|
796
812
|
* registration ship fields whose keys have no text.
|
|
797
813
|
*/
|
|
798
814
|
export { ACP_CLI_SETTINGS_CATALOGS } from "./acp-cli-catalog.ts";
|
|
815
|
+
export { probeSshCliExecutable } from "../remote/ssh-exec.ts";
|
|
799
816
|
|
|
800
817
|
/**
|
|
801
818
|
* The registered instance.
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* models and executed over the Agent Client Protocol:
|
|
7
7
|
*
|
|
8
8
|
* - mode "local": the CLI is spawned on this machine (which/where reachability);
|
|
9
|
-
* - mode "ssh": the CLI runs on a remote host over a direct ssh2 exec,
|
|
10
|
-
* ssh
|
|
9
|
+
* - mode "ssh": the CLI runs on a remote host over a direct ssh2 exec, using
|
|
10
|
+
* either an `/ssh` manager reference or embedded connection fields.
|
|
11
11
|
*
|
|
12
12
|
* Discovery follows the same convention as teammate-models.json / teammate-remotes.json:
|
|
13
13
|
* the global file is ~/.pi/agent/teammate-cli-tools.json and the project file is
|
|
@@ -17,13 +17,16 @@
|
|
|
17
17
|
* ~/.maestro/cli-tools.json for pi-maestro-flow's original provider registration.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { execFileSync } from "node:child_process";
|
|
21
20
|
import * as fs from "node:fs";
|
|
22
21
|
import * as os from "node:os";
|
|
23
22
|
import * as path from "node:path";
|
|
24
23
|
import type { AvailableModelEntry } from "../models/model-catalog.ts";
|
|
25
24
|
import type { RemoteHostConfig } from "../remote/types.ts";
|
|
26
|
-
import {
|
|
25
|
+
import { resolveSshHostRef } from "../public/v1/ssh-hosts.ts";
|
|
26
|
+
import {
|
|
27
|
+
probeSshCliExecutable,
|
|
28
|
+
remoteHostConfigFromSshProfile,
|
|
29
|
+
} from "../remote/ssh-exec.ts";
|
|
27
30
|
|
|
28
31
|
/** Launch configuration for a teammate-managed CLI tool. */
|
|
29
32
|
export interface CliToolConfig {
|
|
@@ -38,7 +41,8 @@ export interface CliToolConfig {
|
|
|
38
41
|
cwd?: string;
|
|
39
42
|
/** Trusted environment-variable names forwarded from the parent process. */
|
|
40
43
|
env?: readonly string[];
|
|
41
|
-
// SSH
|
|
44
|
+
// SSH source (mode "ssh" only): a manager reference OR embedded fields.
|
|
45
|
+
sshHostRef?: string;
|
|
42
46
|
host?: string;
|
|
43
47
|
user?: string;
|
|
44
48
|
port?: number;
|
|
@@ -82,7 +86,6 @@ export interface MaestroDelegateConfig {
|
|
|
82
86
|
|
|
83
87
|
const GLOBAL_CONFIG_FILE = "teammate-cli-tools.json";
|
|
84
88
|
const LEGACY_CONFIG_PATH = path.join(os.homedir(), ".maestro", "cli-tools.json");
|
|
85
|
-
const PROBE_TIMEOUT_MS = 2_000;
|
|
86
89
|
const PROBE_CACHE_TTL_MS = 30_000;
|
|
87
90
|
|
|
88
91
|
export function getGlobalCliToolsConfigPath(): string {
|
|
@@ -237,24 +240,73 @@ export function cliToolArgv(name: string, config: CliToolConfig): [string, ...st
|
|
|
237
240
|
return [cliToolCommand(name, config), ...(config.args ?? [])];
|
|
238
241
|
}
|
|
239
242
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
const SSH_INLINE_FIELDS: readonly (keyof CliToolConfig)[] = [
|
|
244
|
+
"host",
|
|
245
|
+
"user",
|
|
246
|
+
"port",
|
|
247
|
+
"hostKeySha256",
|
|
248
|
+
"identityFile",
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
function hasInlineSshField(config: CliToolConfig): boolean {
|
|
252
|
+
return SSH_INLINE_FIELDS.some((key) => {
|
|
253
|
+
const value = config[key];
|
|
254
|
+
return typeof value === "number" || (typeof value === "string" && value.trim().length > 0);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Safe configuration diagnostic for one ssh-mode tool, or undefined when complete. */
|
|
259
|
+
export function sshHostConfigIssue(config: CliToolConfig): string | undefined {
|
|
260
|
+
if ((config.mode ?? "local") !== "ssh") return undefined;
|
|
261
|
+
const rawRef = config.sshHostRef;
|
|
262
|
+
const hostRef = rawRef?.trim() ?? "";
|
|
263
|
+
if (rawRef !== undefined && hostRef.length === 0) {
|
|
264
|
+
return "sshHostRef must be a non-empty /ssh host id";
|
|
265
|
+
}
|
|
266
|
+
if (hostRef && hasInlineSshField(config)) {
|
|
267
|
+
return "sshHostRef cannot be combined with host, user, port, hostKeySha256 or identityFile";
|
|
268
|
+
}
|
|
269
|
+
if (hostRef) return undefined;
|
|
270
|
+
|
|
243
271
|
const host = config.host?.trim() ?? "";
|
|
244
272
|
const user = config.user?.trim() ?? "";
|
|
245
|
-
const port = config.port ?? 22;
|
|
246
273
|
const hostKeySha256 = config.hostKeySha256?.trim() ?? "";
|
|
247
|
-
if (!host || !user || !hostKeySha256)
|
|
248
|
-
|
|
274
|
+
if (!host || !user || !hostKeySha256) {
|
|
275
|
+
return "ssh mode requires sshHostRef or host, user and hostKeySha256";
|
|
276
|
+
}
|
|
277
|
+
const port = config.port ?? 22;
|
|
278
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
279
|
+
return "ssh port must be an integer between 1 and 65535";
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Embedded SSH connection fields lifted from a tool config; null for references or invalid input. */
|
|
285
|
+
export function sshHostConfigOf(config: CliToolConfig): RemoteHostConfig | null {
|
|
286
|
+
if (sshHostConfigIssue(config) !== undefined || (config.sshHostRef?.trim() ?? "")) return null;
|
|
287
|
+
if ((config.mode ?? "local") !== "ssh") return null;
|
|
249
288
|
return {
|
|
250
|
-
host,
|
|
251
|
-
user,
|
|
252
|
-
port,
|
|
253
|
-
hostKeySha256,
|
|
289
|
+
host: config.host!.trim(),
|
|
290
|
+
user: config.user!.trim(),
|
|
291
|
+
port: config.port ?? 22,
|
|
292
|
+
hostKeySha256: config.hostKeySha256!.trim(),
|
|
254
293
|
...(config.identityFile?.trim() ? { identityFile: config.identityFile.trim() } : {}),
|
|
255
294
|
};
|
|
256
295
|
}
|
|
257
296
|
|
|
297
|
+
/** Resolve either host source immediately before a new direct-SSH connection. */
|
|
298
|
+
export async function resolveSshHostConfigOf(config: CliToolConfig): Promise<RemoteHostConfig> {
|
|
299
|
+
const issue = sshHostConfigIssue(config);
|
|
300
|
+
if (issue !== undefined) throw new Error(issue);
|
|
301
|
+
const hostRef = config.sshHostRef?.trim();
|
|
302
|
+
if (hostRef) {
|
|
303
|
+
return remoteHostConfigFromSshProfile(await resolveSshHostRef(hostRef));
|
|
304
|
+
}
|
|
305
|
+
const inline = sshHostConfigOf(config);
|
|
306
|
+
if (!inline) throw new Error("ssh mode requires sshHostRef or complete embedded SSH fields");
|
|
307
|
+
return inline;
|
|
308
|
+
}
|
|
309
|
+
|
|
258
310
|
export interface CliToolProbeResult {
|
|
259
311
|
ok: boolean;
|
|
260
312
|
command: string;
|
|
@@ -280,6 +332,10 @@ const sshProbeInflight = new Map<string, Promise<void>>();
|
|
|
280
332
|
*/
|
|
281
333
|
function probeCacheKey(command: string, config: CliToolConfig): string {
|
|
282
334
|
if ((config.mode ?? "local") !== "ssh") return JSON.stringify(["local", command]);
|
|
335
|
+
const hostRef = config.sshHostRef?.trim();
|
|
336
|
+
if (hostRef && sshHostConfigIssue(config) === undefined) {
|
|
337
|
+
return JSON.stringify(["ssh-ref", hostRef, command]);
|
|
338
|
+
}
|
|
283
339
|
const hostConfig = sshHostConfigOf(config);
|
|
284
340
|
// An incomplete ssh config has no target to name; the verdict is the same
|
|
285
341
|
// refusal for every such registration, so they share one entry.
|
|
@@ -326,6 +382,34 @@ function adapterInstallHint(command: string): string {
|
|
|
326
382
|
return pkg === undefined ? "" : `; install it with: npm i -g ${pkg}`;
|
|
327
383
|
}
|
|
328
384
|
|
|
385
|
+
function localExecutableCandidates(command: string): string[] {
|
|
386
|
+
const hasPathSeparator = command.includes("/") || command.includes("\\");
|
|
387
|
+
const directories = hasPathSeparator
|
|
388
|
+
? [process.cwd()]
|
|
389
|
+
: (process.env.PATH ?? "").split(path.delimiter).map((entry) => {
|
|
390
|
+
const trimmed = entry.trim();
|
|
391
|
+
return trimmed.replace(/^"|"$/g, "") || process.cwd();
|
|
392
|
+
});
|
|
393
|
+
const extensions = process.platform === "win32" && path.extname(command) === ""
|
|
394
|
+
? ["", ...(process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";")]
|
|
395
|
+
: [""];
|
|
396
|
+
return [...new Set(directories.flatMap((directory) =>
|
|
397
|
+
extensions.map((extension) => path.resolve(directory, `${command}${extension.toLowerCase()}`)),
|
|
398
|
+
))];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function localExecutableExists(command: string): boolean {
|
|
402
|
+
return localExecutableCandidates(command).some((candidate) => {
|
|
403
|
+
try {
|
|
404
|
+
if (!fs.statSync(candidate).isFile()) return false;
|
|
405
|
+
fs.accessSync(candidate, process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
|
|
406
|
+
return true;
|
|
407
|
+
} catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
329
413
|
function probeLocalExecutable(key: string, command: string): CliToolProbeResult {
|
|
330
414
|
const quotedCommand = JSON.stringify(command);
|
|
331
415
|
let result: CliToolProbeResult;
|
|
@@ -351,40 +435,29 @@ function probeLocalExecutable(key: string, command: string): CliToolProbeResult
|
|
|
351
435
|
};
|
|
352
436
|
}
|
|
353
437
|
} else {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
encoding: "utf8",
|
|
358
|
-
timeout: PROBE_TIMEOUT_MS,
|
|
359
|
-
stdio: ["ignore", "ignore", "pipe"],
|
|
360
|
-
windowsHide: true,
|
|
361
|
-
});
|
|
362
|
-
result = { ok: true, command };
|
|
363
|
-
} catch {
|
|
364
|
-
result = { ok: false, command, error: `executable ${quotedCommand} not found on PATH${adapterInstallHint(command)}` };
|
|
365
|
-
}
|
|
438
|
+
result = localExecutableExists(command)
|
|
439
|
+
? { ok: true, command }
|
|
440
|
+
: { ok: false, command, error: `executable ${quotedCommand} not found on PATH${adapterInstallHint(command)}` };
|
|
366
441
|
}
|
|
367
442
|
probeCache.set(key, { result, at: Date.now() });
|
|
368
443
|
return result;
|
|
369
444
|
}
|
|
370
445
|
|
|
371
446
|
function probeSshTool(key: string, command: string, config: CliToolConfig): CliToolProbeResult {
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
374
|
-
const result: CliToolProbeResult = {
|
|
375
|
-
ok: false,
|
|
376
|
-
command,
|
|
377
|
-
error: "ssh mode requires host, user and hostKeySha256 in teammate-cli-tools.json",
|
|
378
|
-
};
|
|
447
|
+
const issue = sshHostConfigIssue(config);
|
|
448
|
+
if (issue !== undefined) {
|
|
449
|
+
const result: CliToolProbeResult = { ok: false, command, error: issue };
|
|
379
450
|
probeCache.set(key, { result, at: Date.now() });
|
|
380
451
|
return result;
|
|
381
452
|
}
|
|
382
|
-
// Optimistically list a complete
|
|
383
|
-
//
|
|
453
|
+
// Optimistically list a complete source while an actual provider-resolution
|
|
454
|
+
// and remote executable probe warms the cache. This is the same bounded
|
|
455
|
+
// discovery behavior as embedded SSH, not a local catalogue result.
|
|
384
456
|
const result: CliToolProbeResult = { ok: true, command };
|
|
385
457
|
probeCache.set(key, { result, at: Date.now() });
|
|
386
458
|
if (!sshProbeInflight.has(key)) {
|
|
387
|
-
const probe =
|
|
459
|
+
const probe = resolveSshHostConfigOf(config)
|
|
460
|
+
.then((hostConfig) => probeSshCliExecutable(hostConfig, command))
|
|
388
461
|
.then((outcome) => {
|
|
389
462
|
probeCache.set(key, {
|
|
390
463
|
result: outcome.ok
|
|
@@ -393,9 +466,13 @@ function probeSshTool(key: string, command: string, config: CliToolConfig): CliT
|
|
|
393
466
|
at: Date.now(),
|
|
394
467
|
});
|
|
395
468
|
})
|
|
396
|
-
.catch(() => {
|
|
469
|
+
.catch((error: unknown) => {
|
|
397
470
|
probeCache.set(key, {
|
|
398
|
-
result: {
|
|
471
|
+
result: {
|
|
472
|
+
ok: false,
|
|
473
|
+
command,
|
|
474
|
+
error: error instanceof Error ? error.message : `remote executable "${command}" probe failed`,
|
|
475
|
+
},
|
|
399
476
|
at: Date.now(),
|
|
400
477
|
});
|
|
401
478
|
})
|
|
@@ -34,7 +34,7 @@ import type {
|
|
|
34
34
|
import {
|
|
35
35
|
cliToolArgv,
|
|
36
36
|
probeCliToolCommand,
|
|
37
|
-
|
|
37
|
+
resolveSshHostConfigOf,
|
|
38
38
|
type CliToolConfig,
|
|
39
39
|
} from "./cli-tools-config.ts";
|
|
40
40
|
|
|
@@ -174,11 +174,15 @@ export async function runSshCliTool(
|
|
|
174
174
|
params: RunLocalCliToolParams,
|
|
175
175
|
): Promise<CliToolRunResult> {
|
|
176
176
|
const startedAt = Date.now();
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
let hostConfig: Awaited<ReturnType<typeof resolveSshHostConfigOf>>;
|
|
178
|
+
try {
|
|
179
|
+
// Resolve for every new run: locking, deleting, or editing an `/ssh` host
|
|
180
|
+
// therefore affects new connections without disturbing an active channel.
|
|
181
|
+
hostConfig = await resolveSshHostConfigOf(params.config);
|
|
182
|
+
} catch (error) {
|
|
179
183
|
return failedResult(
|
|
180
184
|
startedAt,
|
|
181
|
-
`CLI tool "${params.tool}"
|
|
185
|
+
`CLI tool "${params.tool}" SSH host could not be resolved: ${error instanceof Error ? error.message : "unknown SSH host error"}`,
|
|
182
186
|
);
|
|
183
187
|
}
|
|
184
188
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { CompletionOutboxFileStore } from "./file-store.ts";
|
|
3
|
+
import { getRuntimeWorkspaceIdentity } from "../runtime-broker/private-state.ts";
|
|
4
|
+
|
|
5
|
+
interface ParsedOptions {
|
|
6
|
+
command?: "cleanup";
|
|
7
|
+
workspace: string;
|
|
8
|
+
rootDir?: string;
|
|
9
|
+
apply: boolean;
|
|
10
|
+
json: boolean;
|
|
11
|
+
maxEntries?: number;
|
|
12
|
+
help: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function usage(): string {
|
|
16
|
+
return [
|
|
17
|
+
"Usage: pi-teammate-outbox cleanup [options]",
|
|
18
|
+
"",
|
|
19
|
+
"Safely removes obsolete atomic-replacement remnants while preserving the",
|
|
20
|
+
"latest recoverable transaction for every canonical outbox file.",
|
|
21
|
+
"",
|
|
22
|
+
"Options:",
|
|
23
|
+
" --workspace <path> Workspace whose outbox is cleaned (default: cwd)",
|
|
24
|
+
" --root <path> Completion outbox root directory",
|
|
25
|
+
" --apply Delete candidates; without this flag cleanup is dry-run",
|
|
26
|
+
" --max-entries <n> Refuse scans larger than n entries (default: 100000)",
|
|
27
|
+
" --json Emit one JSON result",
|
|
28
|
+
" --help Show this help",
|
|
29
|
+
"",
|
|
30
|
+
"Exit codes: 0 success, 2 usage error, 3 workspace outbox is busy.",
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parsePositiveInteger(value: string | undefined, option: string): number {
|
|
35
|
+
if (!value || !/^\d+$/.test(value)) throw new Error(`${option} requires a positive integer`);
|
|
36
|
+
const parsed = Number(value);
|
|
37
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1 || parsed > 1_000_000) {
|
|
38
|
+
throw new Error(`${option} must be between 1 and 1000000`);
|
|
39
|
+
}
|
|
40
|
+
return parsed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseOptions(args: readonly string[]): ParsedOptions {
|
|
44
|
+
const first = args[0];
|
|
45
|
+
const command = first === "cleanup" ? first : undefined;
|
|
46
|
+
const help = args.includes("--help") || args.includes("-h");
|
|
47
|
+
const options: ParsedOptions = {
|
|
48
|
+
command,
|
|
49
|
+
workspace: process.cwd(),
|
|
50
|
+
apply: false,
|
|
51
|
+
json: false,
|
|
52
|
+
help,
|
|
53
|
+
};
|
|
54
|
+
for (let index = command ? 1 : (help ? 1 : 0); index < args.length; index += 1) {
|
|
55
|
+
const argument = args[index];
|
|
56
|
+
if (argument === "--help" || argument === "-h") continue;
|
|
57
|
+
if (argument === "--apply") {
|
|
58
|
+
options.apply = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (argument === "--json") {
|
|
62
|
+
options.json = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const value = args[++index];
|
|
66
|
+
if (argument === "--workspace") {
|
|
67
|
+
if (!value) throw new Error("--workspace requires a path");
|
|
68
|
+
options.workspace = path.resolve(value);
|
|
69
|
+
} else if (argument === "--root") {
|
|
70
|
+
if (!value) throw new Error("--root requires a path");
|
|
71
|
+
options.rootDir = path.resolve(value);
|
|
72
|
+
} else if (argument === "--max-entries") {
|
|
73
|
+
options.maxEntries = parsePositiveInteger(value, argument);
|
|
74
|
+
} else {
|
|
75
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return options;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function main(args = process.argv.slice(2)): Promise<number> {
|
|
82
|
+
const options = parseOptions(args);
|
|
83
|
+
if (options.help || !options.command) {
|
|
84
|
+
process.stdout.write(`${usage()}\n`);
|
|
85
|
+
return options.help ? 0 : 2;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const identity = getRuntimeWorkspaceIdentity(options.workspace);
|
|
89
|
+
const store = new CompletionOutboxFileStore({
|
|
90
|
+
...(options.rootDir === undefined ? {} : { rootDir: options.rootDir }),
|
|
91
|
+
});
|
|
92
|
+
const result = await store.cleanupRemnants(identity.workspaceId, {
|
|
93
|
+
apply: options.apply,
|
|
94
|
+
...(options.maxEntries === undefined ? {} : { maxEntries: options.maxEntries }),
|
|
95
|
+
});
|
|
96
|
+
const output = {
|
|
97
|
+
workspace: identity.canonicalPath,
|
|
98
|
+
workspaceId: identity.workspaceId,
|
|
99
|
+
rootDir: store.rootDir,
|
|
100
|
+
...result,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (options.json) {
|
|
104
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
105
|
+
} else if (result.busy) {
|
|
106
|
+
process.stderr.write(`Completion outbox is busy for ${identity.canonicalPath}; nothing was removed.\n`);
|
|
107
|
+
} else {
|
|
108
|
+
process.stdout.write([
|
|
109
|
+
`Completion outbox cleanup: ${result.apply ? "applied" : "dry-run"}`,
|
|
110
|
+
`Workspace: ${identity.canonicalPath}`,
|
|
111
|
+
`Scanned: ${result.scannedEntries} entries (${result.replacementFiles} replacement remnants)`,
|
|
112
|
+
`Preserved: ${result.preservedFiles}`,
|
|
113
|
+
`Candidates: ${result.candidateFiles} files, ${result.candidateBytes} bytes`,
|
|
114
|
+
`Removed: ${result.removedFiles} files, ${result.removedBytes} bytes`,
|
|
115
|
+
...(result.candidateSample.length === 0 ? [] : [
|
|
116
|
+
"Candidate sample:",
|
|
117
|
+
...result.candidateSample.map((candidate) => ` ${candidate}`),
|
|
118
|
+
]),
|
|
119
|
+
...(!result.apply && result.candidateFiles > 0 ? ["Re-run with --apply to remove these candidates."] : []),
|
|
120
|
+
].join("\n") + "\n");
|
|
121
|
+
}
|
|
122
|
+
return result.busy ? 3 : 0;
|
|
123
|
+
}
|
|
@@ -151,15 +151,10 @@ export class CompletionDeliveryCoordinator {
|
|
|
151
151
|
#binding: CompletionSessionBinding | undefined;
|
|
152
152
|
#unsubscribe: (() => void) | undefined;
|
|
153
153
|
#disposed = false;
|
|
154
|
-
// Throttle
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
// a sweep between two reconciles is safe; the next reconcile past this gap
|
|
159
|
-
// reclaims them. This is the only place store.gc() is called — bindSession()
|
|
160
|
-
// relies on this reconcile path for the initial sweep on a fresh binding.
|
|
161
|
-
// Seeded lazily on the first reconcile so the first sweep always runs (a zero
|
|
162
|
-
// seed under a small mocked clock would otherwise skip the first sweep).
|
|
154
|
+
// Throttle GC attempts inside reconcile(). tryGc() also persists a cross-process
|
|
155
|
+
// page/expiry fence, but this local guard prevents one coordinator from
|
|
156
|
+
// immediately reacquiring the maintenance lock while a backlog remains.
|
|
157
|
+
// Seeded lazily so the first reconcile still attempts one non-blocking page.
|
|
163
158
|
#lastReconcileGcAt: number | undefined;
|
|
164
159
|
|
|
165
160
|
constructor(options: CompletionCoordinatorOptions = {}) {
|
|
@@ -331,12 +326,12 @@ export class CompletionDeliveryCoordinator {
|
|
|
331
326
|
// failed" warning. Canonical and legacy roots remain isolated and are
|
|
332
327
|
// swept independently; no alias root is merged or deleted.
|
|
333
328
|
if (this.#lastReconcileGcAt === undefined || now - this.#lastReconcileGcAt >= RECONCILE_GC_MIN_INTERVAL_MS) {
|
|
334
|
-
let
|
|
329
|
+
let attempted = false;
|
|
335
330
|
for (const target of targets) {
|
|
336
331
|
const result = await this.store.tryGc(target.workspaceId);
|
|
337
|
-
if (!result.busy && !result.skipped
|
|
332
|
+
if (!result.busy && !result.skipped) attempted = true;
|
|
338
333
|
}
|
|
339
|
-
if (
|
|
334
|
+
if (attempted) this.#lastReconcileGcAt = now;
|
|
340
335
|
}
|
|
341
336
|
const providers = new Set<CompletionDurabilityProvider>();
|
|
342
337
|
const currentProvider = this.registry.current();
|