@phnx-labs/agents-cli 1.20.68 → 1.20.69
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 +45 -0
- package/README.md +11 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/computer.d.ts +26 -0
- package/dist/commands/computer.js +173 -149
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +88 -6
- package/dist/commands/fork.d.ts +9 -0
- package/dist/commands/fork.js +67 -0
- package/dist/commands/run-account-picker.d.ts +14 -0
- package/dist/commands/run-account-picker.js +131 -0
- package/dist/commands/setup-browser.d.ts +18 -0
- package/dist/commands/setup-browser.js +142 -0
- package/dist/commands/setup-computer.d.ts +19 -0
- package/dist/commands/setup-computer.js +133 -0
- package/dist/commands/setup-share.d.ts +17 -0
- package/dist/commands/setup-share.js +85 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +58 -1
- package/dist/commands/share.d.ts +15 -0
- package/dist/commands/share.js +10 -4
- package/dist/commands/ssh.js +164 -0
- package/dist/commands/view.d.ts +3 -14
- package/dist/commands/view.js +27 -28
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +10 -0
- package/dist/lib/agents.js +32 -0
- package/dist/lib/auth-health.d.ts +103 -0
- package/dist/lib/auth-health.js +232 -0
- package/dist/lib/browser/chrome.d.ts +9 -0
- package/dist/lib/browser/chrome.js +22 -0
- package/dist/lib/computer/download.d.ts +51 -0
- package/dist/lib/computer/download.js +145 -0
- package/dist/lib/computer-rpc.js +9 -3
- package/dist/lib/devices/connect.d.ts +15 -0
- package/dist/lib/devices/connect.js +17 -5
- package/dist/lib/devices/terminfo.d.ts +44 -0
- package/dist/lib/devices/terminfo.js +167 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/rotate.d.ts +12 -8
- package/dist/lib/rotate.js +22 -23
- package/dist/lib/session/fork.d.ts +32 -0
- package/dist/lib/session/fork.js +101 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/usage.d.ts +23 -0
- package/dist/lib/usage.js +84 -0
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SessionMeta } from './types.js';
|
|
2
|
+
/** Agents that `fork` can branch today (see the module doc for why). */
|
|
3
|
+
export declare const FORKABLE_AGENTS: readonly ["claude"];
|
|
4
|
+
/** Whether a session's agent can be forked by {@link forkSession}. */
|
|
5
|
+
export declare function isForkableAgent(agent: string): boolean;
|
|
6
|
+
/** Outcome of a successful fork. */
|
|
7
|
+
export interface ForkResult {
|
|
8
|
+
/** The new session's full id. */
|
|
9
|
+
newId: string;
|
|
10
|
+
/** The new session's short id (first 8 chars), for display/resume. */
|
|
11
|
+
shortId: string;
|
|
12
|
+
/** Absolute path of the copied transcript. */
|
|
13
|
+
filePath: string;
|
|
14
|
+
/** The label applied to the fork. */
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Fork a Claude session into a new, independent one.
|
|
19
|
+
*
|
|
20
|
+
* Copies `source.filePath` to a new `<uuid>.jsonl` beside it, rewrites the
|
|
21
|
+
* embedded session id, registers the new session in the index, and records a
|
|
22
|
+
* `--name`-style label. Returns the new ids/path. Throws if the source
|
|
23
|
+
* transcript is missing.
|
|
24
|
+
*
|
|
25
|
+
* @param source The resolved metadata of the session being forked.
|
|
26
|
+
* @param opts.name Optional explicit label; defaults to `fork of <original>`.
|
|
27
|
+
* @param now ISO timestamp to stamp the fork with (injectable for tests).
|
|
28
|
+
*/
|
|
29
|
+
export declare function forkSession(source: SessionMeta, opts?: {
|
|
30
|
+
name?: string;
|
|
31
|
+
now?: string;
|
|
32
|
+
}): ForkResult;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session forking — branch an existing conversation into a new, independent
|
|
3
|
+
* session that can be continued separately, leaving the original untouched.
|
|
4
|
+
*
|
|
5
|
+
* `resume` continues the SAME conversation (same id, same file — it appends).
|
|
6
|
+
* `fork` copies the transcript under a FRESH session id, so continuing the fork
|
|
7
|
+
* diverges from the original instead of mutating it. This is the "git branch"
|
|
8
|
+
* of conversations.
|
|
9
|
+
*
|
|
10
|
+
* v1 supports Claude, whose session id IS its `<id>.jsonl` filename and which
|
|
11
|
+
* resumes natively via `--resume`. A fork is therefore: copy the transcript to
|
|
12
|
+
* a new-uuid filename in the same directory, rewrite the embedded `sessionId`
|
|
13
|
+
* on each line, register the new session in the index, and label it. Other
|
|
14
|
+
* agents (codex single-file; grok/kimi multi-file; opencode DB-only) are a
|
|
15
|
+
* natural follow-up and are refused up front for now.
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from 'crypto';
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { upsertSession } from './db.js';
|
|
21
|
+
import { recordRunName } from './run-names.js';
|
|
22
|
+
/** Agents that `fork` can branch today (see the module doc for why). */
|
|
23
|
+
export const FORKABLE_AGENTS = ['claude'];
|
|
24
|
+
/** Whether a session's agent can be forked by {@link forkSession}. */
|
|
25
|
+
export function isForkableAgent(agent) {
|
|
26
|
+
return FORKABLE_AGENTS.includes(agent);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Rewrite the per-line `sessionId` field of a Claude JSONL transcript to a new
|
|
30
|
+
* id. Claude resolves a conversation by its filename, so this is belt-and-braces
|
|
31
|
+
* (keeps the in-file id consistent with the new filename); malformed lines are
|
|
32
|
+
* passed through untouched.
|
|
33
|
+
*/
|
|
34
|
+
function rewriteSessionId(transcript, newId) {
|
|
35
|
+
return transcript
|
|
36
|
+
.split('\n')
|
|
37
|
+
.map((line) => {
|
|
38
|
+
if (!line.trim())
|
|
39
|
+
return line;
|
|
40
|
+
try {
|
|
41
|
+
const obj = JSON.parse(line);
|
|
42
|
+
if (typeof obj.sessionId === 'string') {
|
|
43
|
+
obj.sessionId = newId;
|
|
44
|
+
return JSON.stringify(obj);
|
|
45
|
+
}
|
|
46
|
+
return line;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return line;
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.join('\n');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Fork a Claude session into a new, independent one.
|
|
56
|
+
*
|
|
57
|
+
* Copies `source.filePath` to a new `<uuid>.jsonl` beside it, rewrites the
|
|
58
|
+
* embedded session id, registers the new session in the index, and records a
|
|
59
|
+
* `--name`-style label. Returns the new ids/path. Throws if the source
|
|
60
|
+
* transcript is missing.
|
|
61
|
+
*
|
|
62
|
+
* @param source The resolved metadata of the session being forked.
|
|
63
|
+
* @param opts.name Optional explicit label; defaults to `fork of <original>`.
|
|
64
|
+
* @param now ISO timestamp to stamp the fork with (injectable for tests).
|
|
65
|
+
*/
|
|
66
|
+
export function forkSession(source, opts = {}) {
|
|
67
|
+
if (!fs.existsSync(source.filePath)) {
|
|
68
|
+
throw new Error(`transcript not found for session ${source.shortId}: ${source.filePath}`);
|
|
69
|
+
}
|
|
70
|
+
const newId = randomUUID();
|
|
71
|
+
const shortId = newId.slice(0, 8);
|
|
72
|
+
const dir = path.dirname(source.filePath);
|
|
73
|
+
const filePath = path.join(dir, `${newId}.jsonl`);
|
|
74
|
+
const transcript = fs.readFileSync(source.filePath, 'utf-8');
|
|
75
|
+
const rewritten = rewriteSessionId(transcript, newId);
|
|
76
|
+
fs.writeFileSync(filePath, rewritten);
|
|
77
|
+
const original = source.label || source.topic || source.shortId;
|
|
78
|
+
const label = opts.name || `fork of ${original}`;
|
|
79
|
+
// Label sidecar (seeds the DB label; survives rescans until an agent title
|
|
80
|
+
// supersedes it), mirroring `agents run --name`.
|
|
81
|
+
recordRunName({ sessionId: newId, name: label, agent: source.agent, cwd: source.cwd });
|
|
82
|
+
// Register the new session so it resolves immediately (by `agents resume`,
|
|
83
|
+
// `agents sessions`, etc.) without waiting for the next scan.
|
|
84
|
+
const stamp = opts.now ?? new Date().toISOString();
|
|
85
|
+
const meta = {
|
|
86
|
+
...source,
|
|
87
|
+
id: newId,
|
|
88
|
+
shortId,
|
|
89
|
+
filePath,
|
|
90
|
+
label,
|
|
91
|
+
timestamp: stamp,
|
|
92
|
+
lastActivity: stamp,
|
|
93
|
+
// The fork has not opened its own PR / team; drop origin-specific refs.
|
|
94
|
+
prUrl: undefined,
|
|
95
|
+
prNumber: undefined,
|
|
96
|
+
teamOrigin: undefined,
|
|
97
|
+
spawnedTeam: undefined,
|
|
98
|
+
};
|
|
99
|
+
upsertSession(meta, rewritten);
|
|
100
|
+
return { newId, shortId, filePath, label };
|
|
101
|
+
}
|
|
@@ -45,6 +45,7 @@ export declare const loadDaemon: ModuleLoader;
|
|
|
45
45
|
export declare const loadRoutines: ModuleLoader;
|
|
46
46
|
export declare const loadMonitors: ModuleLoader;
|
|
47
47
|
export declare const loadRun: ModuleLoader;
|
|
48
|
+
export declare const loadFork: ModuleLoader;
|
|
48
49
|
export declare const loadDefaults: ModuleLoader;
|
|
49
50
|
export declare const loadModels: ModuleLoader;
|
|
50
51
|
export declare const loadPrune: ModuleLoader;
|
|
@@ -23,6 +23,7 @@ export const loadDaemon = async () => (await import('../../commands/daemon.js'))
|
|
|
23
23
|
export const loadRoutines = async () => (await import('../../commands/routines.js')).registerRoutinesCommands;
|
|
24
24
|
export const loadMonitors = async () => (await import('../../commands/monitors.js')).registerMonitorsCommands;
|
|
25
25
|
export const loadRun = async () => (await import('../../commands/exec.js')).registerRunCommand;
|
|
26
|
+
export const loadFork = async () => (await import('../../commands/fork.js')).registerForkCommand;
|
|
26
27
|
export const loadDefaults = async () => (await import('../../commands/defaults.js')).registerDefaultsCommands;
|
|
27
28
|
export const loadModels = async () => (await import('../../commands/models.js')).registerModelsCommand;
|
|
28
29
|
export const loadPrune = async () => (await import('../../commands/prune.js')).registerPruneCommand;
|
|
@@ -126,6 +127,7 @@ export const COMMAND_LOADERS = {
|
|
|
126
127
|
routines: [loadRoutines],
|
|
127
128
|
monitors: [loadMonitors],
|
|
128
129
|
run: [loadRun],
|
|
130
|
+
fork: [loadFork],
|
|
129
131
|
defaults: [loadDefaults],
|
|
130
132
|
models: [loadModels],
|
|
131
133
|
trash: [loadTrash],
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -184,6 +184,29 @@ export interface DroidBillingLimitsResponse {
|
|
|
184
184
|
} | null;
|
|
185
185
|
} | null;
|
|
186
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Live auth probes — the same authenticated GET the usage fetchers above do,
|
|
189
|
+
* but surfacing the raw HTTP status instead of swallowing 401/expired to null.
|
|
190
|
+
* These back `agents fleet ping` and the fleet auth-health cache: completing a
|
|
191
|
+
* real request is the only proof a token is accepted. The local "signed in"
|
|
192
|
+
* flag cannot tell a revoked-but-unexpired token from a good one. Classification
|
|
193
|
+
* of the returned status into a verdict lives in lib/auth-health.ts (kept there
|
|
194
|
+
* so it stays pure/testable and to avoid an import cycle).
|
|
195
|
+
*/
|
|
196
|
+
export interface ProviderProbe {
|
|
197
|
+
/** HTTP status of the probe request, or null when no request was made (missing/expired token) or the request threw. */
|
|
198
|
+
status: number | null;
|
|
199
|
+
/** Local credential state observed before the request. */
|
|
200
|
+
token: 'present' | 'missing' | 'expired';
|
|
201
|
+
/** Network/parse error message when status is null but a token was present. */
|
|
202
|
+
error?: string;
|
|
203
|
+
}
|
|
204
|
+
/** Probe Claude's OAuth token against the usage endpoint. Refreshes an expired access token (safe for Claude). */
|
|
205
|
+
export declare function probeClaudeStatus(home?: string, cliVersion?: string | null): Promise<ProviderProbe>;
|
|
206
|
+
/** Probe Kimi's OAuth token against the /usages endpoint. Never refreshes (single-use rotation — see getKimiUsageInfo). */
|
|
207
|
+
export declare function probeKimiStatus(home?: string): Promise<ProviderProbe>;
|
|
208
|
+
/** Probe Droid's WorkOS token against the billing-limits endpoint. Never refreshes (single-use rotation — see getDroidUsageInfo). */
|
|
209
|
+
export declare function probeDroidStatus(home?: string): Promise<ProviderProbe>;
|
|
187
210
|
/**
|
|
188
211
|
* Normalize the Factory billing-limits payload into the common UsageWindow
|
|
189
212
|
* shape. Orgs on the legacy (non token-rate-limit) billing model have no
|
package/dist/lib/usage.js
CHANGED
|
@@ -609,6 +609,90 @@ async function getDroidUsageInfo(options) {
|
|
|
609
609
|
return { snapshot: null, error: null };
|
|
610
610
|
}
|
|
611
611
|
}
|
|
612
|
+
/** Probe Claude's OAuth token against the usage endpoint. Refreshes an expired access token (safe for Claude). */
|
|
613
|
+
export async function probeClaudeStatus(home, cliVersion) {
|
|
614
|
+
const oauth = await loadClaudeOauth(home);
|
|
615
|
+
if (!oauth?.accessToken)
|
|
616
|
+
return { status: null, token: 'missing' };
|
|
617
|
+
let access = null;
|
|
618
|
+
try {
|
|
619
|
+
access = await getClaudeAccessToken(oauth, home);
|
|
620
|
+
}
|
|
621
|
+
catch {
|
|
622
|
+
access = null;
|
|
623
|
+
}
|
|
624
|
+
// Fall back to the stored (possibly stale) token so the server returns the
|
|
625
|
+
// real verdict — a 401 here IS the revoked/expired signal we want to surface.
|
|
626
|
+
const bearer = access || oauth.accessToken.trim();
|
|
627
|
+
try {
|
|
628
|
+
const response = await fetch(CLAUDE_USAGE_URL, {
|
|
629
|
+
method: 'GET',
|
|
630
|
+
headers: {
|
|
631
|
+
Authorization: `Bearer ${bearer}`,
|
|
632
|
+
'Content-Type': 'application/json',
|
|
633
|
+
'anthropic-beta': CLAUDE_OAUTH_BETA_HEADER,
|
|
634
|
+
'User-Agent': getClaudeUserAgent(cliVersion),
|
|
635
|
+
},
|
|
636
|
+
signal: AbortSignal.timeout(8000),
|
|
637
|
+
});
|
|
638
|
+
return { status: response.status, token: 'present' };
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
return { status: null, token: 'present', error: err instanceof Error ? err.message : String(err) };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/** Probe Kimi's OAuth token against the /usages endpoint. Never refreshes (single-use rotation — see getKimiUsageInfo). */
|
|
645
|
+
export async function probeKimiStatus(home) {
|
|
646
|
+
const credPath = resolveKimiCredentialPath(home);
|
|
647
|
+
if (!credPath)
|
|
648
|
+
return { status: null, token: 'missing' };
|
|
649
|
+
let accessToken;
|
|
650
|
+
let expiresAt = null;
|
|
651
|
+
try {
|
|
652
|
+
const cred = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
|
|
653
|
+
accessToken = typeof cred?.access_token === 'string' ? cred.access_token : undefined;
|
|
654
|
+
expiresAt = typeof cred?.expires_at === 'number' ? cred.expires_at : null;
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
return { status: null, token: 'missing' };
|
|
658
|
+
}
|
|
659
|
+
if (!accessToken)
|
|
660
|
+
return { status: null, token: 'missing' };
|
|
661
|
+
if (expiresAt !== null && Date.now() / 1000 >= expiresAt)
|
|
662
|
+
return { status: null, token: 'expired' };
|
|
663
|
+
try {
|
|
664
|
+
const response = await fetch(KIMI_USAGES_URL, {
|
|
665
|
+
method: 'GET',
|
|
666
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' },
|
|
667
|
+
signal: AbortSignal.timeout(8000),
|
|
668
|
+
});
|
|
669
|
+
return { status: response.status, token: 'present' };
|
|
670
|
+
}
|
|
671
|
+
catch (err) {
|
|
672
|
+
return { status: null, token: 'present', error: err instanceof Error ? err.message : String(err) };
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/** Probe Droid's WorkOS token against the billing-limits endpoint. Never refreshes (single-use rotation — see getDroidUsageInfo). */
|
|
676
|
+
export async function probeDroidStatus(home) {
|
|
677
|
+
const cred = decryptDroidAuthPayload(home || os.homedir());
|
|
678
|
+
const accessToken = cred?.access_token;
|
|
679
|
+
if (typeof accessToken !== 'string' || !accessToken)
|
|
680
|
+
return { status: null, token: 'missing' };
|
|
681
|
+
const exp = decodeJwtPayload(accessToken)?.exp;
|
|
682
|
+
if (typeof exp === 'number' && Date.now() / 1000 >= exp)
|
|
683
|
+
return { status: null, token: 'expired' };
|
|
684
|
+
try {
|
|
685
|
+
const response = await fetch(DROID_USAGE_URL, {
|
|
686
|
+
method: 'GET',
|
|
687
|
+
headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json' },
|
|
688
|
+
signal: AbortSignal.timeout(8000),
|
|
689
|
+
});
|
|
690
|
+
return { status: response.status, token: 'present' };
|
|
691
|
+
}
|
|
692
|
+
catch (err) {
|
|
693
|
+
return { status: null, token: 'present', error: err instanceof Error ? err.message : String(err) };
|
|
694
|
+
}
|
|
695
|
+
}
|
|
612
696
|
/**
|
|
613
697
|
* Normalize the Factory billing-limits payload into the common UsageWindow
|
|
614
698
|
* shape. Orgs on the legacy (non token-rate-limit) billing model have no
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.69",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|