@phnx-labs/agents-cli 1.22.50 → 1.22.52
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 +219 -0
- package/dist/bootstrap.js +1 -1
- package/dist/commands/attach.js +7 -0
- package/dist/commands/browser.d.ts +10 -0
- package/dist/commands/browser.js +191 -54
- package/dist/commands/config.js +20 -0
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +8 -4
- package/dist/commands/detach.js +1 -1
- package/dist/commands/feedback.js +1 -1
- package/dist/commands/focus.d.ts +1 -10
- package/dist/commands/focus.js +14 -79
- package/dist/commands/go.d.ts +26 -0
- package/dist/commands/go.js +63 -5
- package/dist/commands/menubar.js +6 -4
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/repo.js +31 -3
- package/dist/commands/sessions-resume.d.ts +1 -0
- package/dist/commands/sessions-resume.js +13 -2
- package/dist/commands/sessions-stop.js +1 -1
- package/dist/commands/sessions.d.ts +23 -13
- package/dist/commands/sessions.js +40 -20
- package/dist/commands/setup-browser.d.ts +5 -2
- package/dist/commands/setup-browser.js +14 -29
- package/dist/commands/setup-preferences.d.ts +22 -3
- package/dist/commands/setup-preferences.js +25 -8
- package/dist/commands/share.js +12 -8
- package/dist/commands/status.js +5 -0
- package/dist/commands/sync.js +58 -2
- package/dist/commands/tmux.d.ts +8 -1
- package/dist/commands/tmux.js +167 -17
- package/dist/commands/traces.js +32 -4
- package/dist/lib/browser/ipc.d.ts +44 -0
- package/dist/lib/browser/ipc.js +120 -8
- package/dist/lib/browser/profiles.d.ts +39 -17
- package/dist/lib/browser/profiles.js +51 -52
- package/dist/lib/browser/runtime-state.d.ts +4 -2
- package/dist/lib/browser/runtime-state.js +4 -2
- package/dist/lib/browser/service.js +4 -3
- package/dist/lib/channels/owner-forward.d.ts +88 -0
- package/dist/lib/channels/owner-forward.js +116 -0
- package/dist/lib/channels/owner-sink.js +7 -0
- package/dist/lib/claude-statusline.d.ts +9 -0
- package/dist/lib/claude-statusline.js +45 -4
- package/dist/lib/computer/ssh-tunnel.d.ts +7 -6
- package/dist/lib/computer/ssh-tunnel.js +13 -8
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +9 -2
- package/dist/lib/device-config.js +23 -0
- package/dist/lib/exec.d.ts +8 -0
- package/dist/lib/exec.js +7 -0
- package/dist/lib/factory/snapshot.d.ts +1 -1
- package/dist/lib/factory/snapshot.js +1 -1
- package/dist/lib/feed-broadcast.js +15 -1
- package/dist/lib/git.d.ts +93 -0
- package/dist/lib/git.js +232 -0
- package/dist/lib/helper-download.d.ts +12 -2
- package/dist/lib/helper-download.js +12 -2
- package/dist/lib/installations/migrate.js +4 -4
- package/dist/lib/menubar/install-menubar.d.ts +71 -8
- package/dist/lib/menubar/install-menubar.js +183 -24
- package/dist/lib/monitors/remote.d.ts +18 -1
- package/dist/lib/monitors/remote.js +15 -2
- package/dist/lib/notify.d.ts +7 -0
- package/dist/lib/notify.js +15 -1
- package/dist/lib/session/local-tmux-attach.d.ts +69 -0
- package/dist/lib/session/local-tmux-attach.js +164 -0
- package/dist/lib/session/remote-active.d.ts +8 -0
- package/dist/lib/session/remote-active.js +1 -0
- package/dist/lib/share/publish.d.ts +8 -11
- package/dist/lib/share/publish.js +16 -20
- package/dist/lib/share/worker-template.js +99 -12
- package/dist/lib/star-nudge.d.ts +2 -2
- package/dist/lib/star-nudge.js +2 -2
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +2 -2
- package/dist/lib/sync-status.d.ts +17 -0
- package/dist/lib/sync-status.js +21 -2
- package/dist/lib/tmux/index.d.ts +1 -1
- package/dist/lib/tmux/index.js +1 -1
- package/dist/lib/tmux/session.d.ts +10 -0
- package/dist/lib/tmux/session.js +29 -0
- package/dist/lib/traces/sync.d.ts +30 -0
- package/dist/lib/traces/sync.js +91 -10
- package/package.json +3 -3
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach a LIVE tmux pane on THIS box, with zero SSH — the local half of the
|
|
3
|
+
* PHNX-3292 product rule: "first unique match wins, local before fleet."
|
|
4
|
+
*
|
|
5
|
+
* `agents tmux ls` prints `ag-<agent>-<8hex>` names. A selector that already
|
|
6
|
+
* names one of those panes (the full alias, or its bare 8-hex suffix when it
|
|
7
|
+
* is unique among LIVE local panes) is attached directly — no session index
|
|
8
|
+
* lookup, no fleet SSH fan-out. `sessions resume`, the deprecated `sessions
|
|
9
|
+
* attach`, and `sessions focus` all take this gate before anything else.
|
|
10
|
+
*
|
|
11
|
+
* Lives outside `commands/` (not in focus.ts) so `sessions-resume.ts` and
|
|
12
|
+
* `attach.ts` can both import it without a focus.ts <-> sessions-resume.ts
|
|
13
|
+
* import cycle (focus.ts already imports from sessions-resume.ts).
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
import { attachTmux, ensureSessionHookRepaired, getDefaultSocketPath, hasSession, listSessions, runTmux, teardownIfAgentExited } from '../tmux/index.js';
|
|
18
|
+
import { isAgentTmuxAlias } from './types.js';
|
|
19
|
+
/** Width of `SessionMeta.shortId` / the hex an `ag-<agent>-<8hex>` alias embeds. */
|
|
20
|
+
const SHORT_SESSION_ID_RE = /^[0-9a-f]{8}$/i;
|
|
21
|
+
/** Shape of the tmux alias the CLI mints for an agent session: `ag-<agent>-<shortid>`.
|
|
22
|
+
* Delegates to the one canonical matcher beside the name parsers in active.ts. */
|
|
23
|
+
export function looksLikeTmuxAlias(selector) {
|
|
24
|
+
return isAgentTmuxAlias(selector);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Classify a selector against the live tmux server. Split from the attach so the
|
|
28
|
+
* decision is testable against a real tmux server without replacing the caller's
|
|
29
|
+
* shell (attaching is not something a test can undo).
|
|
30
|
+
*/
|
|
31
|
+
export async function resolveTmuxAliasState(selector, socket) {
|
|
32
|
+
if (!looksLikeTmuxAlias(selector))
|
|
33
|
+
return 'not-an-alias';
|
|
34
|
+
const sock = socket ?? getDefaultSocketPath();
|
|
35
|
+
if (!fs.existsSync(sock))
|
|
36
|
+
return 'no-server';
|
|
37
|
+
if (!(await hasSession(selector, sock).catch(() => false)))
|
|
38
|
+
return 'absent';
|
|
39
|
+
const panes = await runTmux({
|
|
40
|
+
socket: sock,
|
|
41
|
+
args: ['list-panes', '-t', `=${selector}`, '-F', '#{pane_dead}'],
|
|
42
|
+
}).catch(() => undefined);
|
|
43
|
+
const states = (panes?.stdout ?? '').split('\n').map(s => s.trim()).filter(Boolean);
|
|
44
|
+
return states.some(d => d === '0') ? 'live' : 'dead';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A local `ag-<agent>-<8hex>` alias names a pane on THIS box. Attach it
|
|
48
|
+
* without any fleet SSH. `--device` keeps the sweep: the caller scoped
|
|
49
|
+
* identity to another machine.
|
|
50
|
+
*/
|
|
51
|
+
export function shouldAttachLocalTmuxAliasBeforeFleet(selector, hosts) {
|
|
52
|
+
return !!selector && hosts.length === 0 && isAgentTmuxAlias(selector);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Attach a live tmux session named exactly as the selector, without needing the
|
|
56
|
+
* session index to know anything about it.
|
|
57
|
+
*
|
|
58
|
+
* SES-41 requires a `ag-<agent>-<8hex>` tmux alias to resolve, but the alias's
|
|
59
|
+
* hex is the LAUNCH id, not the harness session id, and for a harness that
|
|
60
|
+
* writes no `state/sessions/<pid>.json` record there is no mapping back to a
|
|
61
|
+
* SessionMeta at all. The pane is the thing the user asked for, and its NAME is
|
|
62
|
+
* sufficient to attach it — so an unattributable session is still reachable
|
|
63
|
+
* instead of being a dead end that forces raw `tmux -S … attach`.
|
|
64
|
+
*
|
|
65
|
+
* Returns false when this is not an alias, the server/session is absent, or
|
|
66
|
+
* every pane is dead — the caller then continues to normal id resolution.
|
|
67
|
+
* SES-39's "re-read `pane_dead` immediately before attach" is honoured here:
|
|
68
|
+
* liveness is queried at attach time, not read from a roster.
|
|
69
|
+
*/
|
|
70
|
+
export async function attachLiveTmuxAlias(selector) {
|
|
71
|
+
const socket = getDefaultSocketPath();
|
|
72
|
+
if (await resolveTmuxAliasState(selector, socket) !== 'live')
|
|
73
|
+
return false;
|
|
74
|
+
if (!process.stdout.isTTY) {
|
|
75
|
+
console.error(chalk.red(`"${selector}" is a live tmux session, but attaching needs a TTY.`));
|
|
76
|
+
console.error(chalk.gray(` Run it from a terminal, or: agents tmux attach ${selector}`));
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
// Already inside a tmux client on this socket — which is the normal state for
|
|
81
|
+
// an interactive agent session here — MOVE that client instead of nesting a
|
|
82
|
+
// second one. jumpTo (go.ts) does the same; attaching from inside tmux
|
|
83
|
+
// otherwise stacks clients rather than taking you to the session.
|
|
84
|
+
if (process.env.TMUX) {
|
|
85
|
+
await runTmux({ socket, args: ['switch-client', '-t', `=${selector}`], throwOnError: false }).catch(() => { });
|
|
86
|
+
console.log(chalk.gray(`Switched this tmux client to ${selector}.`));
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
console.log(chalk.gray(`Attaching ${selector} — Ctrl-b d to detach.`));
|
|
90
|
+
// Repair a legacy/stale pane-died hook before handing the session to an
|
|
91
|
+
// attach client — the 5-min daemon reconcile that used to cover this was
|
|
92
|
+
// deleted; attach-time repair is what closes the gap now (RUSH-2435).
|
|
93
|
+
await ensureSessionHookRepaired(selector, socket);
|
|
94
|
+
const code = await attachTmux({ socket, args: ['attach-session', '-t', `=${selector}`] });
|
|
95
|
+
await teardownIfAgentExited(selector, socket);
|
|
96
|
+
process.exitCode = code;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a bare 8-hex selector (`agents tmux ls`'s hex column, typed without
|
|
101
|
+
* the `ag-<agent>-` prefix) against LIVE local panes only. Dead panes matching
|
|
102
|
+
* the suffix are excluded before the uniqueness check — a retained, exited pane
|
|
103
|
+
* must never compete with (or block) a live one for the same short id.
|
|
104
|
+
*
|
|
105
|
+
* Exported for direct testing: `attachLocalLiveSelector` composes this with a
|
|
106
|
+
* real `attachTmux()` call, which a unit test cannot safely invoke (it takes
|
|
107
|
+
* over the terminal).
|
|
108
|
+
*/
|
|
109
|
+
export async function resolveUniqueLocalLiveAliasBySuffix(shortId, socket) {
|
|
110
|
+
const sock = socket ?? getDefaultSocketPath();
|
|
111
|
+
if (!fs.existsSync(sock))
|
|
112
|
+
return { kind: 'none' };
|
|
113
|
+
let sessions;
|
|
114
|
+
try {
|
|
115
|
+
sessions = await listSessions({ socket: sock });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return { kind: 'none' };
|
|
119
|
+
}
|
|
120
|
+
const named = sessions
|
|
121
|
+
.map((session) => session.name)
|
|
122
|
+
.filter((name) => isAgentTmuxAlias(name) && name.toLowerCase().endsWith(shortId.toLowerCase()));
|
|
123
|
+
if (named.length === 0)
|
|
124
|
+
return { kind: 'none' };
|
|
125
|
+
const states = await Promise.all(named.map(async (name) => ({ name, state: await resolveTmuxAliasState(name, sock) })));
|
|
126
|
+
const live = states.filter((entry) => entry.state === 'live').map((entry) => entry.name);
|
|
127
|
+
if (live.length === 0)
|
|
128
|
+
return { kind: 'none' };
|
|
129
|
+
if (live.length > 1)
|
|
130
|
+
return { kind: 'collision', aliases: live };
|
|
131
|
+
return { kind: 'alias', alias: live[0] };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The full local gate (PHNX-3292 rule 1): a selector that is either a live
|
|
135
|
+
* local tmux alias, or a bare 8-hex short id that names exactly one live local
|
|
136
|
+
* pane, attaches immediately — zero SSH. `--device`/`hosts` scopes identity to
|
|
137
|
+
* another machine and disables this gate entirely (rule 4).
|
|
138
|
+
*
|
|
139
|
+
* Two LIVE local panes matching the same 8-hex suffix fail closed (rule 5): a
|
|
140
|
+
* collision is reported with both names rather than guessing, and the caller
|
|
141
|
+
* must not fall through to fleet resolution for a selector that is genuinely
|
|
142
|
+
* ambiguous ON THIS BOX.
|
|
143
|
+
*
|
|
144
|
+
* Returns `false` (never attached, never reported) when the selector does not
|
|
145
|
+
* name a live local pane at all, so the caller can continue to session-id
|
|
146
|
+
* resolution / the fleet race.
|
|
147
|
+
*/
|
|
148
|
+
export async function attachLocalLiveSelector(selector, hosts) {
|
|
149
|
+
if (shouldAttachLocalTmuxAliasBeforeFleet(selector, hosts)) {
|
|
150
|
+
return attachLiveTmuxAlias(selector);
|
|
151
|
+
}
|
|
152
|
+
if (!selector || hosts.length > 0 || !SHORT_SESSION_ID_RE.test(selector))
|
|
153
|
+
return false;
|
|
154
|
+
const found = await resolveUniqueLocalLiveAliasBySuffix(selector);
|
|
155
|
+
if (found.kind === 'none')
|
|
156
|
+
return false;
|
|
157
|
+
if (found.kind === 'collision') {
|
|
158
|
+
console.error(chalk.red(`"${selector}" matches ${found.aliases.length} live local panes: ${found.aliases.join(', ')}`));
|
|
159
|
+
console.error(chalk.gray(' Pass the full alias to disambiguate — see: agents tmux ls'));
|
|
160
|
+
process.exitCode = 1;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
return attachLiveTmuxAlias(found.alias);
|
|
164
|
+
}
|
|
@@ -39,4 +39,12 @@ export interface RemoteActiveResult {
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function gatherRemoteActive(hosts?: string[], opts?: {
|
|
41
41
|
quiet?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Opt-in first-hit abort. Omitted by default so `--active`, projects, and
|
|
44
|
+
* `focus` with no id keep all-settle. Detach/stop pass this for a unique
|
|
45
|
+
* live id so a reachable hit does not wait out sleeping peers.
|
|
46
|
+
*/
|
|
47
|
+
earlyExit?: {
|
|
48
|
+
isDefinitive: (item: ActiveSession, machine: string) => boolean;
|
|
49
|
+
};
|
|
42
50
|
}): Promise<RemoteActiveResult>;
|
|
@@ -85,6 +85,8 @@ export interface PublishOptions {
|
|
|
85
85
|
export type ShareVisibility = 'public' | 'unlisted' | 'me' | 'org';
|
|
86
86
|
export interface PublishResult {
|
|
87
87
|
url: string;
|
|
88
|
+
/** URL-safe object name, explicit (`--slug`) or deterministically derived. */
|
|
89
|
+
slug?: string;
|
|
88
90
|
expiresAt?: string;
|
|
89
91
|
coverUrl?: string;
|
|
90
92
|
/** Server-enforced visibility stamped on the object. */
|
|
@@ -224,22 +226,17 @@ export declare function redactEmails(text: string): string;
|
|
|
224
226
|
export declare function scanShareContent(body: string | Buffer): SensitiveHit[];
|
|
225
227
|
/** Build the refuse message when the pre-publish scan finds sensitive content. */
|
|
226
228
|
export declare function formatSensitiveContentError(hits: SensitiveHit[]): string;
|
|
227
|
-
/** Derive a URL-safe slug from a filename
|
|
229
|
+
/** Derive a URL-safe slug from a filename, stripping its path and extension. */
|
|
228
230
|
export declare function slugify(name: string): string;
|
|
229
231
|
/** The project the file belongs to — git repo name, else the cwd's basename. */
|
|
230
232
|
export declare function detectProject(dir?: string): string;
|
|
231
233
|
/**
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* The tail is 8 random bytes (64-bit, 16 hex chars). Reads are public — the URL is
|
|
237
|
-
* the only capability — so the nonce must be genuinely infeasible to brute-force,
|
|
238
|
-
* not merely unlisted; 64 bits puts a blind guess out of reach. (See docs/distribution.md
|
|
239
|
-
* §Security for the threat model and `--expire` for sensitive content.)
|
|
240
|
-
* `--slug` still publishes that exact slug, no view-id appended.
|
|
234
|
+
* Stable default slug for an artifact: prefer its HTML `<title>` or Markdown
|
|
235
|
+
* frontmatter `title:`, then fall back to its filename. The same artifact title
|
|
236
|
+
* always yields the same slug, so republishing without `--slug` updates the same
|
|
237
|
+
* URL. `--slug` remains an exact override.
|
|
241
238
|
*/
|
|
242
|
-
export declare function defaultSlug(filePath: string,
|
|
239
|
+
export declare function defaultSlug(filePath: string, body?: Buffer): string;
|
|
243
240
|
/**
|
|
244
241
|
* Best-effort OG cover: capture a screenshot, upload it as `<slug>.png`, and return
|
|
245
242
|
* the page body with og:image meta injected (+ the cover URL). All IO is injected
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
import { readFileSync } from 'node:fs';
|
|
9
9
|
import { basename } from 'node:path';
|
|
10
10
|
import { execFileSync } from 'node:child_process';
|
|
11
|
-
import { randomBytes } from 'node:crypto';
|
|
12
11
|
import { hostname as osHostname } from 'node:os';
|
|
13
12
|
import { readShareConfig } from './config.js';
|
|
14
13
|
import { resolveGitHubUsername } from '../git.js';
|
|
@@ -309,15 +308,17 @@ export function formatSensitiveContentError(hits) {
|
|
|
309
308
|
`Shares are world-readable by URL — pass --force to publish anyway, ` +
|
|
310
309
|
`or --unlisted --expire 12h to bound the blast radius.`);
|
|
311
310
|
}
|
|
312
|
-
/**
|
|
313
|
-
|
|
314
|
-
return (
|
|
315
|
-
.replace(/\.[^.]+$/, '')
|
|
316
|
-
.toLowerCase()
|
|
311
|
+
/** Normalize artifact title text into a URL-safe slug. */
|
|
312
|
+
function normalizeSlug(name) {
|
|
313
|
+
return (name.toLowerCase()
|
|
317
314
|
.replace(/[^a-z0-9]+/g, '-')
|
|
318
315
|
.replace(/^-+|-+$/g, '')
|
|
319
316
|
.slice(0, 60) || 'page');
|
|
320
317
|
}
|
|
318
|
+
/** Derive a URL-safe slug from a filename, stripping its path and extension. */
|
|
319
|
+
export function slugify(name) {
|
|
320
|
+
return normalizeSlug(basename(name).replace(/\.[^.]+$/, ''));
|
|
321
|
+
}
|
|
321
322
|
function sanitizeSlugPart(s) {
|
|
322
323
|
return sanitizeShareNamespace(s);
|
|
323
324
|
}
|
|
@@ -343,19 +344,13 @@ export function detectProject(dir = process.cwd()) {
|
|
|
343
344
|
return gitRepoName(dir) ?? (sanitizeSlugPart(basename(dir)) || 'share');
|
|
344
345
|
}
|
|
345
346
|
/**
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* The tail is 8 random bytes (64-bit, 16 hex chars). Reads are public — the URL is
|
|
351
|
-
* the only capability — so the nonce must be genuinely infeasible to brute-force,
|
|
352
|
-
* not merely unlisted; 64 bits puts a blind guess out of reach. (See docs/distribution.md
|
|
353
|
-
* §Security for the threat model and `--expire` for sensitive content.)
|
|
354
|
-
* `--slug` still publishes that exact slug, no view-id appended.
|
|
347
|
+
* Stable default slug for an artifact: prefer its HTML `<title>` or Markdown
|
|
348
|
+
* frontmatter `title:`, then fall back to its filename. The same artifact title
|
|
349
|
+
* always yields the same slug, so republishing without `--slug` updates the same
|
|
350
|
+
* URL. `--slug` remains an exact override.
|
|
355
351
|
*/
|
|
356
|
-
export function defaultSlug(filePath,
|
|
357
|
-
|
|
358
|
-
return `${feature}-${randomBytes(8).toString('hex')}`;
|
|
352
|
+
export function defaultSlug(filePath, body) {
|
|
353
|
+
return body ? normalizeSlug(deriveLabel(filePath, body)) : slugify(filePath);
|
|
359
354
|
}
|
|
360
355
|
function guessContentType(filePath) {
|
|
361
356
|
if (/\.html?$/i.test(filePath))
|
|
@@ -459,7 +454,8 @@ export async function publishFile(filePath, opts = {}) {
|
|
|
459
454
|
}
|
|
460
455
|
export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
461
456
|
const username = await resolveShareUsername(opts);
|
|
462
|
-
|
|
457
|
+
let body = readFileSync(filePath);
|
|
458
|
+
const slugPart = (opts.slug ?? defaultSlug(filePath, body)).replace(/^\/+/, '');
|
|
463
459
|
const key = buildShareKey(username, slugPart);
|
|
464
460
|
const expiresAt = resolveExpire(opts.expire);
|
|
465
461
|
const visibility = resolveShareVisibility(opts);
|
|
@@ -472,7 +468,6 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
|
472
468
|
const res = await fetch(u, { method: 'PUT', headers: h, body: new Uint8Array(b) });
|
|
473
469
|
return { ok: res.ok, status: res.status, url: u };
|
|
474
470
|
});
|
|
475
|
-
let body = readFileSync(filePath);
|
|
476
471
|
let coverUrl;
|
|
477
472
|
const isHtml = /\.html?$/i.test(filePath);
|
|
478
473
|
if (isHtml) {
|
|
@@ -576,6 +571,7 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
|
576
571
|
}
|
|
577
572
|
return {
|
|
578
573
|
url: r.url ?? pageUrl,
|
|
574
|
+
slug: key.slice(key.indexOf('/') + 1),
|
|
579
575
|
expiresAt,
|
|
580
576
|
coverUrl,
|
|
581
577
|
label,
|
|
@@ -253,7 +253,20 @@ export default {
|
|
|
253
253
|
headers.set('cache-control', 'public, max-age=60');
|
|
254
254
|
if (visibility === 'unlisted') headers.set('X-Robots-Tag', 'noindex');
|
|
255
255
|
}
|
|
256
|
-
|
|
256
|
+
if (request.method === 'HEAD') return new Response(null, { status: 200, headers });
|
|
257
|
+
// HTML pages get an attribution bar injected at serve time (who shared it,
|
|
258
|
+
// what made it, when, and — the point — a visual VISIBILITY cue). All read
|
|
259
|
+
// from the object's stamped metadata; non-HTML assets (images, JSON, the OG
|
|
260
|
+
// cover) are served byte-for-byte. The body changes, so the R2 etag no
|
|
261
|
+
// longer matches — drop it rather than serve a lying validator.
|
|
262
|
+
const isHtmlPage = (headers.get('content-type') || '').indexOf('text/html') !== -1;
|
|
263
|
+
if (isHtmlPage) {
|
|
264
|
+
const rawHtml = await obj.text();
|
|
265
|
+
const withBar = injectAttributionBar(rawHtml, obj.customMetadata || {}, firstSeg);
|
|
266
|
+
headers.delete('etag');
|
|
267
|
+
return new Response(withBar, { status: 200, headers });
|
|
268
|
+
}
|
|
269
|
+
return new Response(obj.body, { status: 200, headers });
|
|
257
270
|
}
|
|
258
271
|
|
|
259
272
|
if (request.method === 'DELETE') {
|
|
@@ -473,6 +486,68 @@ function escapeHtml(s) {
|
|
|
473
486
|
.replace(/"/g, '"');
|
|
474
487
|
}
|
|
475
488
|
|
|
489
|
+
// The attribution bar: a slim strip prepended to every served HTML page that
|
|
490
|
+
// makes the object's stamped metadata visible — who shared it, what made it,
|
|
491
|
+
// when, and (the whole point) a color-coded VISIBILITY cue Google-Drive style.
|
|
492
|
+
// All values come from customMetadata; nothing new is stamped at publish time,
|
|
493
|
+
// so this is a pure Worker change deployable with 'agents artifacts share update'.
|
|
494
|
+
var VIS_ICON = {
|
|
495
|
+
me: '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="11" width="16" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>',
|
|
496
|
+
org: '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 21h18M6 21V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v17M14 9h4a1 1 0 0 1 1 1v11"/></svg>',
|
|
497
|
+
unlisted: '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1"/><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1"/></svg>',
|
|
498
|
+
public: '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 0 1 0 18M12 3a15 15 0 0 0 0 18"/></svg>',
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
function visibilityChip(visibility, orgDomain) {
|
|
502
|
+
if (visibility === 'me') return { icon: VIS_ICON.me, label: 'Only you', color: '#f59e0b' };
|
|
503
|
+
if (visibility === 'org') return { icon: VIS_ICON.org, label: 'Anyone at ' + escapeHtml(orgDomain || 'your organization'), color: '#5b9dff' };
|
|
504
|
+
if (visibility === 'unlisted') return { icon: VIS_ICON.unlisted, label: 'Unlisted', color: '#9aa0a6' };
|
|
505
|
+
return { icon: VIS_ICON.public, label: 'Public', color: '#30a46c' };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function renderAttributionBar(meta, handle) {
|
|
509
|
+
var cm = meta || {};
|
|
510
|
+
var chip = visibilityChip(cm.visibility || 'public', cm.org_domain);
|
|
511
|
+
var left = '';
|
|
512
|
+
// The handle is already the public URL namespace, so surfacing it leaks nothing new.
|
|
513
|
+
if (handle) left += 'Shared by <strong>' + escapeHtml(handle) + '</strong>';
|
|
514
|
+
if (cm.agent) left += (left ? '<span class="ash-dot">·</span>' : '') + 'Made with ' + escapeHtml(cm.agent);
|
|
515
|
+
var right = '<span class="ash-chip" style="color:' + chip.color + ';border-color:' + chip.color + '66">' + chip.icon + '<span>' + chip.label + '</span></span>';
|
|
516
|
+
if (cm.date) right += '<span class="ash-date">' + escapeHtml(cm.date) + '</span>';
|
|
517
|
+
// The load-bearing background + base colour ride an INLINE style so the host
|
|
518
|
+
// page's own CSS can never wash the bar out (inline beats a page stylesheet);
|
|
519
|
+
// the rest is a namespaced style block with !important on every colour so a
|
|
520
|
+
// broad page rule (span selector to black, etc.) cannot make the text vanish.
|
|
521
|
+
return '<div class="agents-share-bar" role="contentinfo" aria-label="Sharing details" ' +
|
|
522
|
+
'style="background:#0b0b0c !important;color:#e8e8e8 !important">' +
|
|
523
|
+
'<style>' +
|
|
524
|
+
'.agents-share-bar{all:initial;position:sticky;top:0;left:0;right:0;z-index:2147483647;box-sizing:border-box;display:flex !important;align-items:center;gap:12px;flex-wrap:wrap;' +
|
|
525
|
+
'padding:8px 16px !important;background:#0b0b0c !important;color:#e8e8e8 !important;border-bottom:1px solid #24242b !important;' +
|
|
526
|
+
'font:13px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,system-ui,sans-serif !important}' +
|
|
527
|
+
'.agents-share-bar *{box-sizing:border-box;font-family:inherit}' +
|
|
528
|
+
'.agents-share-bar .ash-left{display:flex;align-items:center;gap:8px;min-width:0;color:#cfd3d9 !important}' +
|
|
529
|
+
'.agents-share-bar strong{color:#fff !important;font-weight:600}' +
|
|
530
|
+
'.agents-share-bar .ash-dot{opacity:.4;margin:0 3px}' +
|
|
531
|
+
'.agents-share-bar .ash-right{margin-left:auto;display:flex;align-items:center;gap:12px}' +
|
|
532
|
+
'.agents-share-bar .ash-date{color:#9096a0 !important}' +
|
|
533
|
+
'.agents-share-bar .ash-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:999px;border:1px solid;font-weight:600;font-size:12px;white-space:nowrap}' +
|
|
534
|
+
'.agents-share-bar .ash-chip svg{flex:none}' +
|
|
535
|
+
'</style>' +
|
|
536
|
+
'<span class="ash-left">' + left + '</span>' +
|
|
537
|
+
'<span class="ash-right">' + right + '</span>' +
|
|
538
|
+
'</div>';
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function injectAttributionBar(html, meta, handle) {
|
|
542
|
+
var bar = renderAttributionBar(meta, handle);
|
|
543
|
+
var m = /<body[^>]*>/i.exec(html);
|
|
544
|
+
if (m) {
|
|
545
|
+
var at = m.index + m[0].length;
|
|
546
|
+
return html.slice(0, at) + bar + html.slice(at);
|
|
547
|
+
}
|
|
548
|
+
return bar + html;
|
|
549
|
+
}
|
|
550
|
+
|
|
476
551
|
function json(body, status) {
|
|
477
552
|
return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
|
|
478
553
|
}
|
|
@@ -507,23 +582,35 @@ function phoenixHandle(auth) {
|
|
|
507
582
|
// First writer of a handle owns it. Later PUTs from the same userId are fine;
|
|
508
583
|
// a different userId whose email local-part collides gets 409, not a silent overwrite.
|
|
509
584
|
async function assertHandleOwner(bucket, handle, userId) {
|
|
510
|
-
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
585
|
+
// The __handles/<handle> claim object is the authoritative first-writer record
|
|
586
|
+
// of ownership. When it exists it decides ownership OUTRIGHT: the recorded
|
|
587
|
+
// userId may write, anyone else is refused. Consult it FIRST — a stray page
|
|
588
|
+
// under the namespace stamped with a different owner (e.g. a BYO WRITE_TOKEN
|
|
589
|
+
// publish, which stamps owner = SHARE_NAMESPACE rather than a userId, or a page
|
|
590
|
+
// published under the same human's earlier userId before a re-auth) must not
|
|
591
|
+
// lock the rightful claim holder out of their own handle (PHNX-3291).
|
|
592
|
+
const key = '__handles/' + handle;
|
|
593
|
+
const existing = await bucket.get(key);
|
|
594
|
+
if (existing) {
|
|
595
|
+
const claimed = existing.customMetadata && existing.customMetadata.userId;
|
|
596
|
+
if (claimed && claimed !== userId) {
|
|
597
|
+
return { error: json({ error: 'handle taken', handle: handle }, 409) };
|
|
598
|
+
}
|
|
599
|
+
return {};
|
|
600
|
+
}
|
|
601
|
+
// No claim object yet — fall back to the page-owner scan so a colliding email
|
|
602
|
+
// local-part cannot PUT/DELETE another account's objects in the window before
|
|
603
|
+
// the claim exists. (claimHandle writes the claim on the first Phoenix PUT.)
|
|
604
|
+
// Only a DIFFERENT Phoenix userId blocks: a BYO WRITE_TOKEN publish stamps
|
|
605
|
+
// owner = the namespace (=== handle) rather than a userId, so it is not a rival
|
|
606
|
+
// identity and must not lock the handle's first Phoenix claimant out (PHNX-3291).
|
|
513
607
|
const list = await bucket.list({ prefix: handle + '/', include: ['customMetadata'] });
|
|
514
608
|
for (const o of list.objects || []) {
|
|
515
609
|
const owner = o.customMetadata && o.customMetadata.owner;
|
|
516
|
-
if (owner && owner !== userId) {
|
|
610
|
+
if (owner && owner !== userId && owner !== handle) {
|
|
517
611
|
return { error: json({ error: 'handle taken', handle: handle }, 409) };
|
|
518
612
|
}
|
|
519
613
|
}
|
|
520
|
-
const key = '__handles/' + handle;
|
|
521
|
-
const existing = await bucket.get(key);
|
|
522
|
-
if (!existing) return {};
|
|
523
|
-
const claimed = existing.customMetadata && existing.customMetadata.userId;
|
|
524
|
-
if (claimed && claimed !== userId) {
|
|
525
|
-
return { error: json({ error: 'handle taken', handle: handle }, 409) };
|
|
526
|
-
}
|
|
527
614
|
return {};
|
|
528
615
|
}
|
|
529
616
|
|
package/dist/lib/star-nudge.d.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* repeats. It is a plain inline line — not a toast, not a nag — and stays out
|
|
9
9
|
* of the way of non-interactive, CI, quiet, and JSON output.
|
|
10
10
|
*/
|
|
11
|
-
/** Canonical GitHub repo the nudge points at. */
|
|
12
|
-
export declare const REPO_URL = "https://github.com/phnx-labs/
|
|
11
|
+
/** Canonical GitHub repo the nudge points at (renamed from agents-cli). */
|
|
12
|
+
export declare const REPO_URL = "https://github.com/phnx-labs/agi-cli";
|
|
13
13
|
/** Has the star nudge already been shown on this machine? */
|
|
14
14
|
export declare function hasShownStarNudge(): boolean;
|
|
15
15
|
/** Inputs to the pure show/skip decision (kept side-effect free for testing). */
|
package/dist/lib/star-nudge.js
CHANGED
|
@@ -12,8 +12,8 @@ import * as fs from 'fs';
|
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
14
|
import { getRuntimeStateDir } from './state.js';
|
|
15
|
-
/** Canonical GitHub repo the nudge points at. */
|
|
16
|
-
export const REPO_URL = 'https://github.com/phnx-labs/
|
|
15
|
+
/** Canonical GitHub repo the nudge points at (renamed from agents-cli). */
|
|
16
|
+
export const REPO_URL = 'https://github.com/phnx-labs/agi-cli';
|
|
17
17
|
/** Sentinel written the first (and only) time the nudge is shown. */
|
|
18
18
|
function nudgeSentinelPath() {
|
|
19
19
|
return path.join(getRuntimeStateDir(), 'star-nudge-shown');
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare function getHumansFilePath(): string;
|
|
|
31
31
|
* file against `schema/agents-yaml.schema.json`. Exported so
|
|
32
32
|
* `lib/devices/config-migration.ts` rewrites a device doc with the same header.
|
|
33
33
|
*/
|
|
34
|
-
export declare const META_HEADER = "# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/
|
|
34
|
+
export declare const META_HEADER = "# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agi-cli\n# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/agi-cli/main/cli/schema/agents-yaml.schema.json\n\n";
|
|
35
35
|
/** Root of the system data directory (~/.agents/.system/). */
|
|
36
36
|
export declare function getAgentsDir(): string;
|
|
37
37
|
/** Root of the system data directory (~/.agents/.system/). */
|
package/dist/lib/state.js
CHANGED
|
@@ -167,8 +167,8 @@ const USER_PROMPTCUTS_FILE = path.join(USER_AGENTS_DIR, 'hooks', 'promptcuts.yam
|
|
|
167
167
|
*/
|
|
168
168
|
export const META_HEADER = `# agents-cli metadata
|
|
169
169
|
# Auto-generated - do not edit manually
|
|
170
|
-
# https://github.com/phnx-labs/
|
|
171
|
-
# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/
|
|
170
|
+
# https://github.com/phnx-labs/agi-cli
|
|
171
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/phnx-labs/agi-cli/main/cli/schema/agents-yaml.schema.json
|
|
172
172
|
|
|
173
173
|
`;
|
|
174
174
|
// ─── Root getters ─────────────────────────────────────────────────────────────
|
|
@@ -64,8 +64,18 @@ export interface SystemRepoStatus {
|
|
|
64
64
|
/** True when the dir isn't a git repo or has no upstream — behind is unknown. */
|
|
65
65
|
unknown: boolean;
|
|
66
66
|
}
|
|
67
|
+
export interface UserRepoStatus {
|
|
68
|
+
dir: string;
|
|
69
|
+
/**
|
|
70
|
+
* True when `~/.agents` exists but is not a git repo (or is a repo with no
|
|
71
|
+
* `origin`) — a partial install `agents repo sync user` will adopt in place
|
|
72
|
+
* (PHNX-3301). A DISTINCT drift state, not a per-agent "N missing" count.
|
|
73
|
+
*/
|
|
74
|
+
notGitRepo: boolean;
|
|
75
|
+
}
|
|
67
76
|
export interface UnifiedSyncStatus {
|
|
68
77
|
system: SystemRepoStatus;
|
|
78
|
+
user: UserRepoStatus;
|
|
69
79
|
agents: AgentVersionStatus[];
|
|
70
80
|
totals: {
|
|
71
81
|
drifted: number;
|
|
@@ -93,6 +103,13 @@ export interface SyncStatusOptions {
|
|
|
93
103
|
* the menu-bar surfaces; we read it once, here, so every surface agrees.
|
|
94
104
|
*/
|
|
95
105
|
export declare function getSystemRepoStatus(): Promise<SystemRepoStatus>;
|
|
106
|
+
/**
|
|
107
|
+
* Detect whether `~/.agents` (the user config layer) is git-backed. A partial
|
|
108
|
+
* install — runtime state present but no `.git` (or no `origin`) — is a distinct
|
|
109
|
+
* drift state that `agents repo sync user` heals by adopting in place (PHNX-3301),
|
|
110
|
+
* surfaced separately from per-version resource gaps. Purely local; no network.
|
|
111
|
+
*/
|
|
112
|
+
export declare function getUserRepoStatus(): Promise<UserRepoStatus>;
|
|
96
113
|
/**
|
|
97
114
|
* Compute unified sync status across the fleet. Resolves against non-project
|
|
98
115
|
* layers only (`excludeProject: true`) — the GLOBAL version home is never
|
package/dist/lib/sync-status.js
CHANGED
|
@@ -23,8 +23,9 @@ import { ALL_AGENT_IDS } from './agents.js';
|
|
|
23
23
|
import { diffVersionResources, } from './doctor-diff.js';
|
|
24
24
|
import { listInstalledVersions, getGlobalDefault } from './installations/versions.js';
|
|
25
25
|
import { loadManifest } from './staleness/index.js';
|
|
26
|
-
import { getSystemAgentsDir } from './state.js';
|
|
27
|
-
import
|
|
26
|
+
import { getSystemAgentsDir, getUserAgentsDir } from './state.js';
|
|
27
|
+
import * as fs from 'fs';
|
|
28
|
+
import { isGitRepo, readOriginUrl } from './git.js';
|
|
28
29
|
const STATUS_MAP = {
|
|
29
30
|
ok: 'synced',
|
|
30
31
|
diff: 'drifted',
|
|
@@ -73,6 +74,22 @@ export async function getSystemRepoStatus() {
|
|
|
73
74
|
return base;
|
|
74
75
|
}
|
|
75
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Detect whether `~/.agents` (the user config layer) is git-backed. A partial
|
|
79
|
+
* install — runtime state present but no `.git` (or no `origin`) — is a distinct
|
|
80
|
+
* drift state that `agents repo sync user` heals by adopting in place (PHNX-3301),
|
|
81
|
+
* surfaced separately from per-version resource gaps. Purely local; no network.
|
|
82
|
+
*/
|
|
83
|
+
export async function getUserRepoStatus() {
|
|
84
|
+
const dir = getUserAgentsDir();
|
|
85
|
+
if (!fs.existsSync(dir))
|
|
86
|
+
return { dir, notGitRepo: false };
|
|
87
|
+
if (!isGitRepo(dir))
|
|
88
|
+
return { dir, notGitRepo: true };
|
|
89
|
+
// A repo with no `origin` is just as partial for adopt's purposes — reuse the
|
|
90
|
+
// single origin-URL reader rather than a second remote check.
|
|
91
|
+
return { dir, notGitRepo: readOriginUrl(dir) === null };
|
|
92
|
+
}
|
|
76
93
|
/**
|
|
77
94
|
* Compute unified sync status across the fleet. Resolves against non-project
|
|
78
95
|
* layers only (`excludeProject: true`) — the GLOBAL version home is never
|
|
@@ -107,6 +124,7 @@ export async function computeSyncStatus(options = {}) {
|
|
|
107
124
|
}
|
|
108
125
|
}
|
|
109
126
|
const system = await getSystemRepoStatus();
|
|
127
|
+
const user = await getUserRepoStatus();
|
|
110
128
|
const agentsNeedingSync = new Set();
|
|
111
129
|
let drifted = 0, missing = 0, orphan = 0, versionsNeedingSync = 0, versionsNeverSynced = 0;
|
|
112
130
|
for (const v of agents) {
|
|
@@ -122,6 +140,7 @@ export async function computeSyncStatus(options = {}) {
|
|
|
122
140
|
}
|
|
123
141
|
return {
|
|
124
142
|
system,
|
|
143
|
+
user,
|
|
125
144
|
agents,
|
|
126
145
|
totals: {
|
|
127
146
|
drifted,
|
package/dist/lib/tmux/index.d.ts
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { findTmuxBinary, isTmuxInstalled, getTmuxVersion, isTmuxVersionSupported, MIN_TMUX_VERSION, assertTmuxAvailable, TmuxUnavailableError, TmuxCommandError, runTmux, attachTmux, } from './binary.js';
|
|
7
7
|
export { getDefaultSocketPath, getSessionMetaPath, ensureTmuxDir, } from './paths.js';
|
|
8
|
-
export { assertValidSessionName, slugifyName, hasSession, createSession, killSession, killAll, listSessions, splitPane, sendKeys, capturePane, readSessionMeta, TmuxSessionError, reconcileSessionHooks, ensureSessionHookRepaired, type SessionMeta, type CreateSessionOptions, type ListedSession, type SplitOptions, type SendOptions, type CaptureOptions, } from './session.js';
|
|
8
|
+
export { assertValidSessionName, slugifyName, hasSession, createSession, killSession, teardownIfAgentExited, killAll, listSessions, splitPane, sendKeys, capturePane, readSessionMeta, TmuxSessionError, reconcileSessionHooks, ensureSessionHookRepaired, type SessionMeta, type CreateSessionOptions, type ListedSession, type SplitOptions, type SendOptions, type CaptureOptions, } from './session.js';
|
package/dist/lib/tmux/index.js
CHANGED
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { findTmuxBinary, isTmuxInstalled, getTmuxVersion, isTmuxVersionSupported, MIN_TMUX_VERSION, assertTmuxAvailable, TmuxUnavailableError, TmuxCommandError, runTmux, attachTmux, } from './binary.js';
|
|
7
7
|
export { getDefaultSocketPath, getSessionMetaPath, ensureTmuxDir, } from './paths.js';
|
|
8
|
-
export { assertValidSessionName, slugifyName, hasSession, createSession, killSession, killAll, listSessions, splitPane, sendKeys, capturePane, readSessionMeta, TmuxSessionError, reconcileSessionHooks, ensureSessionHookRepaired, } from './session.js';
|
|
8
|
+
export { assertValidSessionName, slugifyName, hasSession, createSession, killSession, teardownIfAgentExited, killAll, listSessions, splitPane, sendKeys, capturePane, readSessionMeta, TmuxSessionError, reconcileSessionHooks, ensureSessionHookRepaired, } from './session.js';
|
|
@@ -109,6 +109,16 @@ export declare function createSession(opts: CreateSessionOptions): Promise<Sessi
|
|
|
109
109
|
export declare function killSession(name: string, socket?: string, opts?: {
|
|
110
110
|
reapOrphans?: boolean;
|
|
111
111
|
}): Promise<boolean>;
|
|
112
|
+
/**
|
|
113
|
+
* After an attach client returns: destroy the session if every pane is dead
|
|
114
|
+
* (the agent exited); leave it if any pane is still alive (Ctrl-b d).
|
|
115
|
+
*
|
|
116
|
+
* `runInTmux` already does this via `resolveAfterAttach`. The attach verbs
|
|
117
|
+
* (`agents tmux attach`, `sessions focus`/`resume --attach-only`, `jumpTo`)
|
|
118
|
+
* used to `process.exit` the tmux client status and leave a `remain-on-exit`
|
|
119
|
+
* husk — the session "came back" in `tmux ls` after the user exited the agent.
|
|
120
|
+
*/
|
|
121
|
+
export declare function teardownIfAgentExited(name: string, socket?: string): Promise<'killed' | 'kept' | 'absent'>;
|
|
112
122
|
/**
|
|
113
123
|
* Kill every session on the shared server AND the server itself, then prune
|
|
114
124
|
* meta files. Wipes the socket so the next `new` starts from a clean slate.
|