@phnx-labs/agents-cli 1.22.25 → 1.22.26
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 +183 -0
- package/README.md +17 -2
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/browser.js +35 -0
- package/dist/commands/doctor.js +8 -0
- package/dist/commands/insights.d.ts +25 -19
- package/dist/commands/insights.js +107 -33
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +105 -46
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +43 -0
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/index.js +1 -1
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +29 -2
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- package/dist/lib/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- package/dist/lib/devices/doctor-findings.d.ts +7 -1
- package/dist/lib/devices/doctor-findings.js +33 -1
- package/dist/lib/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +183 -6
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hosts/passthrough.d.ts +23 -0
- package/dist/lib/hosts/passthrough.js +45 -0
- package/dist/lib/hosts/ready.d.ts +2 -0
- package/dist/lib/hosts/ready.js +10 -1
- package/dist/lib/hosts/reconnect.d.ts +14 -12
- package/dist/lib/hosts/reconnect.js +41 -40
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/routines.js +14 -2
- package/dist/lib/runner.d.ts +0 -3
- package/dist/lib/runner.js +1 -14
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/session/db.d.ts +21 -3
- package/dist/lib/session/db.js +221 -13
- package/dist/lib/session/discover.d.ts +1 -0
- package/dist/lib/session/discover.js +115 -19
- package/dist/lib/session/insights.d.ts +18 -0
- package/dist/lib/session/insights.js +143 -1
- package/dist/lib/session/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- package/dist/lib/ssh-exec.js +8 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +75 -7
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- package/package.json +1 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { RECIPE_IDS, runRecipe,
|
|
2
|
-
export interface
|
|
3
|
-
window:
|
|
1
|
+
import { RECIPE_IDS, runRecipe, analyticsWindow, type RecipeId, type RecipeSection, type AnalyticsWindow } from './recipes.js';
|
|
2
|
+
export interface MixDashboard {
|
|
3
|
+
window: AnalyticsWindow;
|
|
4
4
|
durationMs: number;
|
|
5
5
|
sections: RecipeSection[];
|
|
6
6
|
}
|
|
7
|
-
|
|
7
|
+
/** @deprecated Use MixDashboard. */
|
|
8
|
+
export type TrendsDashboard = MixDashboard;
|
|
9
|
+
export declare function buildMixDashboard(opts?: {
|
|
8
10
|
days?: number;
|
|
9
11
|
ids?: RecipeId[];
|
|
10
|
-
}):
|
|
11
|
-
|
|
12
|
+
}): MixDashboard;
|
|
13
|
+
/** @deprecated Use buildMixDashboard. */
|
|
14
|
+
export declare const buildTrendsDashboard: typeof buildMixDashboard;
|
|
15
|
+
export { analyticsWindow, analyticsWindow as trendsWindow, runRecipe, RECIPE_IDS, type RecipeId, type RecipeSection, type AnalyticsWindow, };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RECIPE_IDS, runRecipe,
|
|
1
|
+
import { RECIPE_IDS, runRecipe, analyticsWindow, } from './recipes.js';
|
|
2
2
|
const DEFAULT_ORDER = [
|
|
3
3
|
'harness-mix',
|
|
4
4
|
'model-mix',
|
|
@@ -9,9 +9,9 @@ const DEFAULT_ORDER = [
|
|
|
9
9
|
'browser-activity',
|
|
10
10
|
'resource-mix',
|
|
11
11
|
];
|
|
12
|
-
export function
|
|
12
|
+
export function buildMixDashboard(opts = {}) {
|
|
13
13
|
const t0 = Date.now();
|
|
14
|
-
const win =
|
|
14
|
+
const win = analyticsWindow(opts.days ?? 7);
|
|
15
15
|
const ids = opts.ids ?? DEFAULT_ORDER;
|
|
16
16
|
const sections = [];
|
|
17
17
|
for (const id of ids) {
|
|
@@ -28,4 +28,6 @@ export function buildTrendsDashboard(opts = {}) {
|
|
|
28
28
|
sections,
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
/** @deprecated Use buildMixDashboard. */
|
|
32
|
+
export const buildTrendsDashboard = buildMixDashboard;
|
|
33
|
+
export { analyticsWindow, analyticsWindow as trendsWindow, runRecipe, RECIPE_IDS, };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counter / warehouse mix recipes under `agents insights`.
|
|
3
|
+
*
|
|
4
|
+
* These used to live as the top-level `agents trends` tree. That name was a
|
|
5
|
+
* peer of `agents insights` with overlapping "analytics" meaning, so agents and
|
|
6
|
+
* humans kept picking the wrong verb. The cheap counter path (sessions index +
|
|
7
|
+
* usage.db) still exists — it is now `agents insights mix` and the recipe
|
|
8
|
+
* subcommands below. Latency stays on `agents perf`; quota on `agents usage`.
|
|
9
|
+
*
|
|
10
|
+
* `agents trends` remains a thin deprecated alias registered from
|
|
11
|
+
* `commands/trends.ts` that shares this same registration — no second copy of
|
|
12
|
+
* the recipe tree.
|
|
13
|
+
*/
|
|
14
|
+
import type { Command } from 'commander';
|
|
15
|
+
export declare function parseMixDays(raw: string | undefined): number;
|
|
16
|
+
export declare function printMixSection(section: {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
rows: Array<Record<string, string | number | null>>;
|
|
20
|
+
}): void;
|
|
21
|
+
export declare function renderMixDashboard(days: number, asJson: boolean, bannerLabel?: string): void;
|
|
22
|
+
/** One-line deprecation for the retired top-level `agents trends` spelling. */
|
|
23
|
+
export declare function printTrendsDeprecation(): void;
|
|
24
|
+
export interface RegisterMixCommandsOptions {
|
|
25
|
+
/**
|
|
26
|
+
* When true, every action prints the trends deprecation line first. Used only
|
|
27
|
+
* by the thin top-level `agents trends` alias — the real tree under insights
|
|
28
|
+
* never sets this.
|
|
29
|
+
*/
|
|
30
|
+
deprecated?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Banner label for the multi-recipe dashboard (default: agents insights mix).
|
|
33
|
+
* The deprecated alias passes `agents insights mix` still, so callers learn
|
|
34
|
+
* the new name even when they typed trends.
|
|
35
|
+
*/
|
|
36
|
+
dashboardBanner?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Attach counter-mix subcommands to a parent (typically `insights` or the
|
|
40
|
+
* deprecated `trends` alias).
|
|
41
|
+
*
|
|
42
|
+
* Layout:
|
|
43
|
+
* <parent> mix multi-recipe board (default for trends alias)
|
|
44
|
+
* <parent> recipes list recipe ids
|
|
45
|
+
* <parent> query raw usage.db rows
|
|
46
|
+
* <parent> harness-mix|… one baked recipe
|
|
47
|
+
*/
|
|
48
|
+
export declare function registerMixCommands(parent: Command, opts?: RegisterMixCommandsOptions): void;
|
|
49
|
+
/**
|
|
50
|
+
* Thin top-level `agents trends` alias: same mix tree, deprecation on every path,
|
|
51
|
+
* default action = mix board (old `agents trends` with no subcommand).
|
|
52
|
+
*/
|
|
53
|
+
export declare function registerDeprecatedTrendsAlias(program: Command): void;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counter / warehouse mix recipes under `agents insights`.
|
|
3
|
+
*
|
|
4
|
+
* These used to live as the top-level `agents trends` tree. That name was a
|
|
5
|
+
* peer of `agents insights` with overlapping "analytics" meaning, so agents and
|
|
6
|
+
* humans kept picking the wrong verb. The cheap counter path (sessions index +
|
|
7
|
+
* usage.db) still exists — it is now `agents insights mix` and the recipe
|
|
8
|
+
* subcommands below. Latency stays on `agents perf`; quota on `agents usage`.
|
|
9
|
+
*
|
|
10
|
+
* `agents trends` remains a thin deprecated alias registered from
|
|
11
|
+
* `commands/trends.ts` that shares this same registration — no second copy of
|
|
12
|
+
* the recipe tree.
|
|
13
|
+
*/
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { setHelpSections } from '../help.js';
|
|
16
|
+
import { buildMixDashboard, analyticsWindow, runRecipe, RECIPE_IDS, } from './dashboard.js';
|
|
17
|
+
import { listRecipes } from './recipes.js';
|
|
18
|
+
import { queryUsage, usageDbPath, USAGE_KINDS } from './usage-db.js';
|
|
19
|
+
export function parseMixDays(raw) {
|
|
20
|
+
const n = parseInt(raw ?? '7', 10);
|
|
21
|
+
return Number.isFinite(n) && n > 0 ? n : 7;
|
|
22
|
+
}
|
|
23
|
+
function parseLimit(raw, fallback) {
|
|
24
|
+
const n = parseInt(raw ?? String(fallback), 10);
|
|
25
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
26
|
+
}
|
|
27
|
+
export function printMixSection(section) {
|
|
28
|
+
console.log(chalk.bold(section.title));
|
|
29
|
+
if (section.rows.length === 0) {
|
|
30
|
+
console.log(chalk.gray(' (no data)'));
|
|
31
|
+
console.log();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const keys = Object.keys(section.rows[0]);
|
|
35
|
+
const widths = keys.map((k) => Math.max(k.length, ...section.rows.map((r) => String(r[k] ?? '').length), 4));
|
|
36
|
+
const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
|
|
37
|
+
console.log(chalk.gray(keys.map((k, i) => pad(k.toUpperCase(), widths[i])).join(' ')));
|
|
38
|
+
for (const row of section.rows) {
|
|
39
|
+
console.log(keys.map((k, i) => pad(String(row[k] ?? ''), widths[i])).join(' '));
|
|
40
|
+
}
|
|
41
|
+
console.log();
|
|
42
|
+
}
|
|
43
|
+
export function renderMixDashboard(days, asJson, bannerLabel = 'agents insights mix') {
|
|
44
|
+
const dash = buildMixDashboard({ days });
|
|
45
|
+
if (asJson) {
|
|
46
|
+
console.log(JSON.stringify(dash, null, 2));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
console.log(chalk.bold(`${bannerLabel} — last ${dash.window.days} days`));
|
|
50
|
+
console.log(chalk.gray(`compute ${dash.durationMs}ms · usage ${usageDbPath()}`));
|
|
51
|
+
console.log();
|
|
52
|
+
if (dash.sections.length === 0) {
|
|
53
|
+
console.log(chalk.gray('No session or usage data in this window yet.'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const section of dash.sections)
|
|
57
|
+
printMixSection(section);
|
|
58
|
+
}
|
|
59
|
+
/** One-line deprecation for the retired top-level `agents trends` spelling. */
|
|
60
|
+
export function printTrendsDeprecation() {
|
|
61
|
+
console.error(chalk.yellow('agents trends is deprecated — use `agents insights mix` (or `agents insights <recipe>`).'));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Attach counter-mix subcommands to a parent (typically `insights` or the
|
|
65
|
+
* deprecated `trends` alias).
|
|
66
|
+
*
|
|
67
|
+
* Layout:
|
|
68
|
+
* <parent> mix multi-recipe board (default for trends alias)
|
|
69
|
+
* <parent> recipes list recipe ids
|
|
70
|
+
* <parent> query raw usage.db rows
|
|
71
|
+
* <parent> harness-mix|… one baked recipe
|
|
72
|
+
*/
|
|
73
|
+
export function registerMixCommands(parent, opts = {}) {
|
|
74
|
+
const before = () => {
|
|
75
|
+
if (opts.deprecated)
|
|
76
|
+
printTrendsDeprecation();
|
|
77
|
+
};
|
|
78
|
+
const banner = opts.dashboardBanner ?? 'agents insights mix';
|
|
79
|
+
const mix = parent
|
|
80
|
+
.command('mix')
|
|
81
|
+
.description('Counter recipes — harness/model mix, token ratios, resource frequency (sessions index + usage.db)')
|
|
82
|
+
.option('--days <n>', 'Days of history to include', '7')
|
|
83
|
+
.option('--json', 'Emit JSON instead of tables')
|
|
84
|
+
.action(function summary() {
|
|
85
|
+
before();
|
|
86
|
+
const o = this.opts();
|
|
87
|
+
renderMixDashboard(parseMixDays(o.days), Boolean(o.json), banner);
|
|
88
|
+
});
|
|
89
|
+
setHelpSections(mix, {
|
|
90
|
+
examples: `
|
|
91
|
+
# Auto recipe board (7d)
|
|
92
|
+
agents insights mix
|
|
93
|
+
|
|
94
|
+
# Last 30 days
|
|
95
|
+
agents insights mix --days 30
|
|
96
|
+
|
|
97
|
+
# One recipe as JSON
|
|
98
|
+
agents insights harness-mix --json
|
|
99
|
+
|
|
100
|
+
# Raw usage events
|
|
101
|
+
agents insights query --kind secret --days 7
|
|
102
|
+
|
|
103
|
+
# List baked recipe ids
|
|
104
|
+
agents insights recipes
|
|
105
|
+
`,
|
|
106
|
+
notes: `
|
|
107
|
+
Session recipes read sessions.db; resource recipes read ~/.agents/.history/analytics/usage.db.
|
|
108
|
+
Empty recipes are skipped on the default mix board.
|
|
109
|
+
This is the cheap counter path. Behavioural report (transcript content, account split)
|
|
110
|
+
is bare \`agents insights\`. Latency is \`agents perf\`; quota is \`agents usage\`.
|
|
111
|
+
Skill/slash-command popularity is \`agents sessions stats\`.
|
|
112
|
+
`,
|
|
113
|
+
});
|
|
114
|
+
parent.command('recipes')
|
|
115
|
+
.description('List baked mix-recipe ids')
|
|
116
|
+
.option('--json', 'Emit JSON')
|
|
117
|
+
.action((o) => {
|
|
118
|
+
before();
|
|
119
|
+
const list = listRecipes();
|
|
120
|
+
if (o.json) {
|
|
121
|
+
console.log(JSON.stringify(list, null, 2));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
for (const r of list) {
|
|
125
|
+
console.log(`${r.id.padEnd(22)} ${r.store.padEnd(10)} ${r.title}`);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
parent.command('query')
|
|
129
|
+
.description('Raw usage-event query (usage.db)')
|
|
130
|
+
.option('--kind <kind>', `One of: ${USAGE_KINDS.join(', ')}`)
|
|
131
|
+
.option('--name <name>', 'Resource name filter')
|
|
132
|
+
.option('--event <event>', 'Event name filter')
|
|
133
|
+
.option('--days <n>', 'Days of history', '7')
|
|
134
|
+
.option('--limit <n>', 'Max rows', '40')
|
|
135
|
+
.option('--json', 'Emit JSON')
|
|
136
|
+
.action((o) => {
|
|
137
|
+
before();
|
|
138
|
+
const win = analyticsWindow(parseMixDays(o.days));
|
|
139
|
+
const kind = o.kind && USAGE_KINDS.includes(o.kind)
|
|
140
|
+
? o.kind
|
|
141
|
+
: undefined;
|
|
142
|
+
if (o.kind && !kind) {
|
|
143
|
+
console.error(`Unknown kind '${o.kind}'. Expected: ${USAGE_KINDS.join(', ')}`);
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const rows = queryUsage({
|
|
148
|
+
kind,
|
|
149
|
+
name: o.name,
|
|
150
|
+
event: o.event,
|
|
151
|
+
sinceIso: win.sinceIso,
|
|
152
|
+
limit: parseLimit(o.limit, 40),
|
|
153
|
+
});
|
|
154
|
+
if (o.json) {
|
|
155
|
+
console.log(JSON.stringify({ window: win, rows }, null, 2));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (rows.length === 0) {
|
|
159
|
+
console.log(chalk.gray('No usage events.'));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
printMixSection({
|
|
163
|
+
id: 'query',
|
|
164
|
+
title: 'Usage events',
|
|
165
|
+
rows: rows.map((r) => ({
|
|
166
|
+
ts: r.ts,
|
|
167
|
+
kind: r.kind,
|
|
168
|
+
name: r.name,
|
|
169
|
+
event: r.event,
|
|
170
|
+
agent: r.agent,
|
|
171
|
+
})),
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
for (const id of RECIPE_IDS) {
|
|
175
|
+
parent.command(id)
|
|
176
|
+
.description(`Mix recipe: ${id}`)
|
|
177
|
+
.option('--days <n>', 'Days of history', '7')
|
|
178
|
+
.option('--json', 'Emit JSON')
|
|
179
|
+
.action(function recipeAction() {
|
|
180
|
+
before();
|
|
181
|
+
const parentOpts = this.parent?.opts?.();
|
|
182
|
+
const o = { ...parentOpts, ...this.opts() };
|
|
183
|
+
const win = analyticsWindow(parseMixDays(o.days));
|
|
184
|
+
const section = runRecipe(id, win);
|
|
185
|
+
if (o.json) {
|
|
186
|
+
console.log(JSON.stringify({ window: win, section }, null, 2));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (section.empty) {
|
|
190
|
+
console.log(chalk.gray(`No data for recipe '${id}' in the last ${win.days} days.`));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
printMixSection(section);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Thin top-level `agents trends` alias: same mix tree, deprecation on every path,
|
|
199
|
+
* default action = mix board (old `agents trends` with no subcommand).
|
|
200
|
+
*/
|
|
201
|
+
export function registerDeprecatedTrendsAlias(program) {
|
|
202
|
+
const trends = program
|
|
203
|
+
.command('trends')
|
|
204
|
+
.description('Deprecated alias of `agents insights mix` — counter recipes over sessions + usage.db')
|
|
205
|
+
.option('--days <n>', 'Days of history to include', '7')
|
|
206
|
+
.option('--json', 'Emit JSON instead of tables')
|
|
207
|
+
.action(function summary() {
|
|
208
|
+
printTrendsDeprecation();
|
|
209
|
+
const o = this.opts();
|
|
210
|
+
renderMixDashboard(parseMixDays(o.days), Boolean(o.json), 'agents insights mix');
|
|
211
|
+
});
|
|
212
|
+
setHelpSections(trends, {
|
|
213
|
+
examples: `
|
|
214
|
+
# Prefer the canonical spelling
|
|
215
|
+
agents insights mix
|
|
216
|
+
|
|
217
|
+
# Still works (prints a deprecation line)
|
|
218
|
+
agents trends
|
|
219
|
+
agents trends harness-mix --json
|
|
220
|
+
`,
|
|
221
|
+
notes: `
|
|
222
|
+
\`agents trends\` is a compatibility alias. New code and agent briefs should use
|
|
223
|
+
\`agents insights mix\` / \`agents insights <recipe>\`. Behavioural report remains
|
|
224
|
+
bare \`agents insights\`. Latency remains \`agents perf\`.
|
|
225
|
+
`,
|
|
226
|
+
});
|
|
227
|
+
// Same subcommands as insights, with deprecation on every fire.
|
|
228
|
+
registerMixCommands(trends, { deprecated: true, dashboardBanner: 'agents insights mix' });
|
|
229
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { type UsageKind } from './usage-db.js';
|
|
2
|
-
|
|
2
|
+
/** Time window for mix recipes under `agents insights mix`. */
|
|
3
|
+
export interface AnalyticsWindow {
|
|
3
4
|
days: number;
|
|
4
5
|
sinceIso: string;
|
|
5
6
|
}
|
|
7
|
+
/** @deprecated Use AnalyticsWindow — kept as a type alias for external callers. */
|
|
8
|
+
export type TrendsWindow = AnalyticsWindow;
|
|
6
9
|
export interface RecipeSection {
|
|
7
10
|
id: string;
|
|
8
11
|
title: string;
|
|
@@ -10,23 +13,25 @@ export interface RecipeSection {
|
|
|
10
13
|
rows: Array<Record<string, string | number | null>>;
|
|
11
14
|
empty?: boolean;
|
|
12
15
|
}
|
|
13
|
-
export declare function
|
|
14
|
-
|
|
15
|
-
export declare
|
|
16
|
-
export declare function
|
|
17
|
-
export declare function
|
|
18
|
-
export declare function
|
|
19
|
-
export declare function
|
|
20
|
-
export declare function
|
|
21
|
-
export declare function
|
|
16
|
+
export declare function analyticsWindow(days: number): AnalyticsWindow;
|
|
17
|
+
/** @deprecated Use analyticsWindow. */
|
|
18
|
+
export declare const trendsWindow: typeof analyticsWindow;
|
|
19
|
+
export declare function recipeHarnessMix(win: AnalyticsWindow): RecipeSection;
|
|
20
|
+
export declare function recipeModelMix(win: AnalyticsWindow): RecipeSection;
|
|
21
|
+
export declare function recipeToolsPerSession(win: AnalyticsWindow): RecipeSection;
|
|
22
|
+
export declare function recipeTokenRatio(win: AnalyticsWindow): RecipeSection;
|
|
23
|
+
export declare function recipeSessionVolume(win: AnalyticsWindow): RecipeSection;
|
|
24
|
+
export declare function recipeSecretsHot(win: AnalyticsWindow): RecipeSection;
|
|
25
|
+
export declare function recipeBrowserActivity(win: AnalyticsWindow): RecipeSection;
|
|
26
|
+
export declare function recipeResourceMix(win: AnalyticsWindow): RecipeSection;
|
|
22
27
|
export type RecipeId = 'harness-mix' | 'model-mix' | 'tools-per-session' | 'token-ratio' | 'session-volume' | 'secrets-hot' | 'browser-activity' | 'resource-mix';
|
|
23
28
|
export declare const RECIPE_IDS: readonly RecipeId[];
|
|
24
|
-
export declare function runRecipe(id: RecipeId, win:
|
|
29
|
+
export declare function runRecipe(id: RecipeId, win: AnalyticsWindow): RecipeSection;
|
|
25
30
|
export declare function listRecipes(): Array<{
|
|
26
31
|
id: RecipeId;
|
|
27
32
|
title: string;
|
|
28
33
|
store: 'sessions' | 'usage';
|
|
29
34
|
}>;
|
|
30
|
-
export declare function hasSessionsInWindow(win:
|
|
31
|
-
export declare function usageKindsPresent(win:
|
|
32
|
-
export declare function usageEventCount(kind: UsageKind, win:
|
|
35
|
+
export declare function hasSessionsInWindow(win: AnalyticsWindow): boolean;
|
|
36
|
+
export declare function usageKindsPresent(win: AnalyticsWindow): UsageKind[];
|
|
37
|
+
export declare function usageEventCount(kind: UsageKind, win: AnalyticsWindow): number;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import Database from '../sqlite.js';
|
|
2
2
|
import { getSessionsDbPath } from '../state.js';
|
|
3
3
|
import { topNamesByKind, kindMix, countUsage, listUsageKindsWithData, } from './usage-db.js';
|
|
4
|
-
export function
|
|
4
|
+
export function analyticsWindow(days) {
|
|
5
5
|
const d = Number.isFinite(days) && days > 0 ? days : 7;
|
|
6
6
|
const since = new Date(Date.now() - d * 24 * 60 * 60 * 1000);
|
|
7
7
|
return { days: d, sinceIso: since.toISOString() };
|
|
8
8
|
}
|
|
9
|
+
/** @deprecated Use analyticsWindow. */
|
|
10
|
+
export const trendsWindow = analyticsWindow;
|
|
9
11
|
function openSessions() {
|
|
10
12
|
try {
|
|
11
13
|
const db = new Database(getSessionsDbPath());
|
|
@@ -290,7 +292,7 @@ export function runRecipe(id, win) {
|
|
|
290
292
|
return RUNNERS[id](win);
|
|
291
293
|
}
|
|
292
294
|
export function listRecipes() {
|
|
293
|
-
const win =
|
|
295
|
+
const win = analyticsWindow(7);
|
|
294
296
|
return RECIPE_IDS.map((id) => {
|
|
295
297
|
const s = RUNNERS[id](win);
|
|
296
298
|
return { id, title: s.title, store: s.store };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as net from 'net';
|
|
1
2
|
import { BrowserService } from './service.js';
|
|
2
3
|
import type { IPCRequest, IPCResponse } from './types.js';
|
|
3
4
|
export interface IPCRequestOptions {
|
|
@@ -11,6 +12,25 @@ export declare function getSocketPath(): string;
|
|
|
11
12
|
/** Is the daemon reachable? A real connect probe on every platform — a socket
|
|
12
13
|
* file existing on disk is not proof a daemon is listening on it. */
|
|
13
14
|
export declare function isDaemonReachable(): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* One long-lived connection to the existing browser daemon. Requests are
|
|
17
|
+
* serialized so the daemon's newline-delimited responses always map to the
|
|
18
|
+
* caller that produced them, while the process and socket stay warm between
|
|
19
|
+
* actions.
|
|
20
|
+
*/
|
|
21
|
+
export declare class BrowserIPCConnection {
|
|
22
|
+
private readonly socket;
|
|
23
|
+
private buffer;
|
|
24
|
+
private pending;
|
|
25
|
+
private tail;
|
|
26
|
+
private closed;
|
|
27
|
+
constructor(socket: net.Socket);
|
|
28
|
+
request(request: IPCRequest): Promise<IPCResponse>;
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
private requestOnce;
|
|
31
|
+
private handleData;
|
|
32
|
+
private fail;
|
|
33
|
+
}
|
|
14
34
|
export declare class BrowserIPCServer {
|
|
15
35
|
private server;
|
|
16
36
|
private service;
|
|
@@ -27,3 +47,9 @@ export declare class BrowserIPCServer {
|
|
|
27
47
|
*/
|
|
28
48
|
export declare function shouldRestartStaleDaemon(daemonVersion: string | undefined, clientVersion: string): boolean;
|
|
29
49
|
export declare function sendIPCRequest(request: IPCRequest, opts?: IPCRequestOptions): Promise<IPCResponse>;
|
|
50
|
+
/**
|
|
51
|
+
* Open one connection that can serve many browser operations. This uses the
|
|
52
|
+
* same daemon readiness and version-reconciliation path as sendIPCRequest;
|
|
53
|
+
* only the client process and IPC socket lifetime differ.
|
|
54
|
+
*/
|
|
55
|
+
export declare function connectBrowserIPC(opts?: IPCRequestOptions): Promise<BrowserIPCConnection>;
|
package/dist/lib/browser/ipc.js
CHANGED
|
@@ -64,6 +64,92 @@ async function waitForSocket(_socketPath, timeoutMs) {
|
|
|
64
64
|
}
|
|
65
65
|
throw new Error('Timeout waiting for browser daemon socket');
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* One long-lived connection to the existing browser daemon. Requests are
|
|
69
|
+
* serialized so the daemon's newline-delimited responses always map to the
|
|
70
|
+
* caller that produced them, while the process and socket stay warm between
|
|
71
|
+
* actions.
|
|
72
|
+
*/
|
|
73
|
+
export class BrowserIPCConnection {
|
|
74
|
+
socket;
|
|
75
|
+
buffer = '';
|
|
76
|
+
pending;
|
|
77
|
+
tail = Promise.resolve();
|
|
78
|
+
closed = false;
|
|
79
|
+
constructor(socket) {
|
|
80
|
+
this.socket = socket;
|
|
81
|
+
socket.on('data', (data) => this.handleData(data));
|
|
82
|
+
socket.on('error', (error) => this.fail(error));
|
|
83
|
+
socket.on('close', () => {
|
|
84
|
+
this.fail(new Error('Browser daemon connection closed'));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
request(request) {
|
|
88
|
+
const result = this.tail.then(() => this.requestOnce(request));
|
|
89
|
+
this.tail = result.then(() => undefined, () => undefined);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
async close() {
|
|
93
|
+
await this.tail;
|
|
94
|
+
if (this.closed || this.socket.destroyed)
|
|
95
|
+
return;
|
|
96
|
+
await new Promise((resolve) => {
|
|
97
|
+
this.socket.once('close', resolve);
|
|
98
|
+
this.socket.end();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
requestOnce(request) {
|
|
102
|
+
if (this.closed || this.socket.destroyed) {
|
|
103
|
+
return Promise.reject(new Error('Browser daemon connection is closed'));
|
|
104
|
+
}
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
this.pending = { resolve, reject };
|
|
107
|
+
this.socket.write(`${JSON.stringify(request)}\n`, (error) => {
|
|
108
|
+
if (!error)
|
|
109
|
+
return;
|
|
110
|
+
if (this.pending?.reject === reject)
|
|
111
|
+
this.pending = undefined;
|
|
112
|
+
reject(error);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
handleData(data) {
|
|
117
|
+
this.buffer += data.toString();
|
|
118
|
+
while (true) {
|
|
119
|
+
const index = this.buffer.indexOf('\n');
|
|
120
|
+
if (index === -1)
|
|
121
|
+
return;
|
|
122
|
+
const line = this.buffer.slice(0, index);
|
|
123
|
+
this.buffer = this.buffer.slice(index + 1);
|
|
124
|
+
if (!line.trim())
|
|
125
|
+
continue;
|
|
126
|
+
const pending = this.pending;
|
|
127
|
+
if (!pending) {
|
|
128
|
+
this.socket.destroy();
|
|
129
|
+
this.fail(new Error('Browser daemon sent an unexpected response'));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.pending = undefined;
|
|
133
|
+
try {
|
|
134
|
+
pending.resolve(JSON.parse(line));
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
const error = new Error('Browser daemon returned invalid JSON');
|
|
138
|
+
pending.reject(error);
|
|
139
|
+
this.socket.destroy();
|
|
140
|
+
this.fail(error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
fail(error) {
|
|
145
|
+
if (this.closed)
|
|
146
|
+
return;
|
|
147
|
+
this.closed = true;
|
|
148
|
+
const pending = this.pending;
|
|
149
|
+
this.pending = undefined;
|
|
150
|
+
pending?.reject(error);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
67
153
|
export class BrowserIPCServer {
|
|
68
154
|
server = null;
|
|
69
155
|
service;
|
|
@@ -527,33 +613,35 @@ async function reconcileDaemonVersion(socketPath) {
|
|
|
527
613
|
export async function sendIPCRequest(request, opts = {}) {
|
|
528
614
|
return sendRawIPCRequest(request, opts);
|
|
529
615
|
}
|
|
616
|
+
/**
|
|
617
|
+
* Open one connection that can serve many browser operations. This uses the
|
|
618
|
+
* same daemon readiness and version-reconciliation path as sendIPCRequest;
|
|
619
|
+
* only the client process and IPC socket lifetime differ.
|
|
620
|
+
*/
|
|
621
|
+
export async function connectBrowserIPC(opts = {}) {
|
|
622
|
+
const autoStartDaemon = opts.autoStartDaemon ?? true;
|
|
623
|
+
await prepareIPC('status', opts);
|
|
624
|
+
return new Promise((resolve, reject) => {
|
|
625
|
+
const socket = net.createConnection(getIpcEndpoint());
|
|
626
|
+
const onError = (error) => {
|
|
627
|
+
socket.destroy();
|
|
628
|
+
if (!autoStartDaemon && (error.code === 'ENOENT' || error.code === 'ECONNREFUSED')) {
|
|
629
|
+
reject(new BrowserDaemonNotRunningError());
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
reject(new Error(`IPC error: ${error.message}`));
|
|
633
|
+
};
|
|
634
|
+
socket.once('error', onError);
|
|
635
|
+
socket.once('connect', () => {
|
|
636
|
+
socket.off('error', onError);
|
|
637
|
+
resolve(new BrowserIPCConnection(socket));
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
}
|
|
530
641
|
async function sendRawIPCRequest(request, opts = {}) {
|
|
531
|
-
const socketPath = getSocketPath();
|
|
532
642
|
const endpoint = getIpcEndpoint();
|
|
533
643
|
const autoStartDaemon = opts.autoStartDaemon ?? true;
|
|
534
|
-
|
|
535
|
-
if (!autoStartDaemon) {
|
|
536
|
-
throw new BrowserDaemonNotRunningError();
|
|
537
|
-
}
|
|
538
|
-
if (!IS_WINDOWS) {
|
|
539
|
-
await fs.promises.mkdir(path.dirname(socketPath), { recursive: true, mode: 0o700 });
|
|
540
|
-
await fs.promises.chmod(path.dirname(socketPath), 0o700);
|
|
541
|
-
}
|
|
542
|
-
startDaemon();
|
|
543
|
-
if (!(await isDaemonReachable())) {
|
|
544
|
-
await waitForSocket(socketPath, 6000);
|
|
545
|
-
}
|
|
546
|
-
if (!(await isDaemonReachable())) {
|
|
547
|
-
throw new Error('Failed to start browser daemon');
|
|
548
|
-
}
|
|
549
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
550
|
-
}
|
|
551
|
-
// Before serving a real request, make sure the daemon isn't running stale
|
|
552
|
-
// code. Skips the internal `version` probe (avoids recursion) and callers
|
|
553
|
-
// that opt out of auto-start. No-ops once reconciled or when versions match.
|
|
554
|
-
if (request.action !== 'version' && autoStartDaemon) {
|
|
555
|
-
await reconcileDaemonVersion(socketPath);
|
|
556
|
-
}
|
|
644
|
+
await prepareIPC(request.action, opts);
|
|
557
645
|
return new Promise((resolve, reject) => {
|
|
558
646
|
const socket = net.createConnection(endpoint);
|
|
559
647
|
let buffer = '';
|
|
@@ -583,3 +671,30 @@ async function sendRawIPCRequest(request, opts = {}) {
|
|
|
583
671
|
});
|
|
584
672
|
});
|
|
585
673
|
}
|
|
674
|
+
async function prepareIPC(action, opts) {
|
|
675
|
+
const socketPath = getSocketPath();
|
|
676
|
+
const autoStartDaemon = opts.autoStartDaemon ?? true;
|
|
677
|
+
if (!(await isDaemonReachable())) {
|
|
678
|
+
if (!autoStartDaemon) {
|
|
679
|
+
throw new BrowserDaemonNotRunningError();
|
|
680
|
+
}
|
|
681
|
+
if (!IS_WINDOWS) {
|
|
682
|
+
await fs.promises.mkdir(path.dirname(socketPath), { recursive: true, mode: 0o700 });
|
|
683
|
+
await fs.promises.chmod(path.dirname(socketPath), 0o700);
|
|
684
|
+
}
|
|
685
|
+
startDaemon();
|
|
686
|
+
if (!(await isDaemonReachable())) {
|
|
687
|
+
await waitForSocket(socketPath, 6000);
|
|
688
|
+
}
|
|
689
|
+
if (!(await isDaemonReachable())) {
|
|
690
|
+
throw new Error('Failed to start browser daemon');
|
|
691
|
+
}
|
|
692
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
693
|
+
}
|
|
694
|
+
// Before serving a real request, make sure the daemon isn't running stale
|
|
695
|
+
// code. Skips the internal `version` probe (avoids recursion) and callers
|
|
696
|
+
// that opt out of auto-start. No-ops once reconciled or when versions match.
|
|
697
|
+
if (action !== 'version' && autoStartDaemon) {
|
|
698
|
+
await reconcileDaemonVersion(socketPath);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
@@ -26,6 +26,17 @@ export declare function getProfileDownloadsDir(name: string): string;
|
|
|
26
26
|
export declare function getProfileSessionsDir(name: string, task: string): string;
|
|
27
27
|
export declare function listProfiles(): Promise<BrowserProfile[]>;
|
|
28
28
|
export declare function getProfile(name: string): Promise<BrowserProfile | null>;
|
|
29
|
+
/**
|
|
30
|
+
* True when `profile` can actually launch or attach on THIS machine.
|
|
31
|
+
*
|
|
32
|
+
* A remote (`ssh://`) profile runs its browser on the far host, so a local
|
|
33
|
+
* binary check doesn't apply — assume launchable. A local profile must have its
|
|
34
|
+
* configured browser/binary present on disk here; a `default` profile
|
|
35
|
+
* auto-created on a different OS (e.g. a macOS Chrome path reused on Linux — the
|
|
36
|
+
* "Custom binary not found" failure) fails this check so the caller re-detects
|
|
37
|
+
* for the current machine.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isProfileLaunchableHere(profile: BrowserProfile): boolean;
|
|
29
40
|
/**
|
|
30
41
|
* Resolve the profile `agents browser start` uses when no `--profile` is given.
|
|
31
42
|
*
|
|
@@ -105,7 +105,7 @@ export async function getProfile(name) {
|
|
|
105
105
|
* "Custom binary not found" failure) fails this check so the caller re-detects
|
|
106
106
|
* for the current machine.
|
|
107
107
|
*/
|
|
108
|
-
function isProfileLaunchableHere(profile) {
|
|
108
|
+
export function isProfileLaunchableHere(profile) {
|
|
109
109
|
const remote = Object.values(getEndpointPresets(profile)).some((preset) => preset.target.startsWith('ssh://'));
|
|
110
110
|
if (remote)
|
|
111
111
|
return true;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Readable, Writable } from 'stream';
|
|
2
|
+
export interface BrowserIPCStreamOptions {
|
|
3
|
+
input: Readable;
|
|
4
|
+
output: Writable;
|
|
5
|
+
task?: string;
|
|
6
|
+
actor: string;
|
|
7
|
+
launchId?: string;
|
|
8
|
+
autoStartDaemon?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read browser IPC requests as NDJSON and write one NDJSON response per line.
|
|
12
|
+
* The Node process and daemon connection stay alive until input closes.
|
|
13
|
+
*/
|
|
14
|
+
export declare function runBrowserIPCStream(options: BrowserIPCStreamOptions): Promise<void>;
|