@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +183 -0
  2. package/README.md +17 -2
  3. package/dist/bin/agents +0 -0
  4. package/dist/browser.js +14 -4
  5. package/dist/commands/apply.js +52 -8
  6. package/dist/commands/browser.js +35 -0
  7. package/dist/commands/doctor.js +8 -0
  8. package/dist/commands/insights.d.ts +25 -19
  9. package/dist/commands/insights.js +107 -33
  10. package/dist/commands/reconnect.d.ts +46 -0
  11. package/dist/commands/reconnect.js +109 -0
  12. package/dist/commands/routines.js +2 -2
  13. package/dist/commands/secrets.d.ts +2 -8
  14. package/dist/commands/secrets.js +29 -105
  15. package/dist/commands/sessions.js +4 -0
  16. package/dist/commands/setup-secrets.d.ts +1 -0
  17. package/dist/commands/setup-secrets.js +1 -1
  18. package/dist/commands/setup.d.ts +26 -3
  19. package/dist/commands/setup.js +105 -46
  20. package/dist/commands/teams.d.ts +6 -0
  21. package/dist/commands/teams.js +43 -0
  22. package/dist/commands/trends.d.ts +8 -0
  23. package/dist/commands/trends.js +10 -156
  24. package/dist/index.js +1 -1
  25. package/dist/lib/agents.d.ts +11 -0
  26. package/dist/lib/agents.js +29 -2
  27. package/dist/lib/analytics/dashboard.d.ts +10 -6
  28. package/dist/lib/analytics/dashboard.js +6 -4
  29. package/dist/lib/analytics/mix-commands.d.ts +53 -0
  30. package/dist/lib/analytics/mix-commands.js +229 -0
  31. package/dist/lib/analytics/recipes.d.ts +19 -14
  32. package/dist/lib/analytics/recipes.js +4 -2
  33. package/dist/lib/browser/ipc.d.ts +26 -0
  34. package/dist/lib/browser/ipc.js +139 -24
  35. package/dist/lib/browser/profiles.d.ts +11 -0
  36. package/dist/lib/browser/profiles.js +1 -1
  37. package/dist/lib/browser/stream.d.ts +14 -0
  38. package/dist/lib/browser/stream.js +71 -0
  39. package/dist/lib/channels/owner-sink.d.ts +27 -0
  40. package/dist/lib/channels/owner-sink.js +93 -0
  41. package/dist/lib/devices/doctor-findings.d.ts +7 -1
  42. package/dist/lib/devices/doctor-findings.js +33 -1
  43. package/dist/lib/fleet/apply.d.ts +59 -3
  44. package/dist/lib/fleet/apply.js +183 -6
  45. package/dist/lib/fleet/types.d.ts +21 -2
  46. package/dist/lib/hooks/cache.js +15 -0
  47. package/dist/lib/hosts/passthrough.d.ts +23 -0
  48. package/dist/lib/hosts/passthrough.js +45 -0
  49. package/dist/lib/hosts/ready.d.ts +2 -0
  50. package/dist/lib/hosts/ready.js +10 -1
  51. package/dist/lib/hosts/reconnect.d.ts +14 -12
  52. package/dist/lib/hosts/reconnect.js +41 -40
  53. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  54. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  55. package/dist/lib/routines.js +14 -2
  56. package/dist/lib/runner.d.ts +0 -3
  57. package/dist/lib/runner.js +1 -14
  58. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  59. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  60. package/dist/lib/secrets/push.d.ts +94 -0
  61. package/dist/lib/secrets/push.js +145 -0
  62. package/dist/lib/secrets/reaper.d.ts +15 -1
  63. package/dist/lib/secrets/reaper.js +30 -3
  64. package/dist/lib/session/db.d.ts +21 -3
  65. package/dist/lib/session/db.js +221 -13
  66. package/dist/lib/session/discover.d.ts +1 -0
  67. package/dist/lib/session/discover.js +115 -19
  68. package/dist/lib/session/insights.d.ts +18 -0
  69. package/dist/lib/session/insights.js +143 -1
  70. package/dist/lib/session/tool-index.js +133 -22
  71. package/dist/lib/session/tool-store.d.ts +26 -2
  72. package/dist/lib/session/tool-store.js +36 -17
  73. package/dist/lib/ssh-exec.js +8 -2
  74. package/dist/lib/startup/command-registry.d.ts +1 -0
  75. package/dist/lib/startup/command-registry.js +4 -0
  76. package/dist/lib/teams/agents.d.ts +13 -0
  77. package/dist/lib/teams/agents.js +75 -7
  78. package/dist/lib/teams/placement-probe.d.ts +21 -0
  79. package/dist/lib/teams/placement-probe.js +135 -0
  80. package/dist/lib/teams/scheduler.d.ts +74 -1
  81. package/dist/lib/teams/scheduler.js +187 -10
  82. package/package.json +1 -1
@@ -1,11 +1,15 @@
1
- import { RECIPE_IDS, runRecipe, trendsWindow, type RecipeId, type RecipeSection, type TrendsWindow } from './recipes.js';
2
- export interface TrendsDashboard {
3
- window: TrendsWindow;
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
- export declare function buildTrendsDashboard(opts?: {
7
+ /** @deprecated Use MixDashboard. */
8
+ export type TrendsDashboard = MixDashboard;
9
+ export declare function buildMixDashboard(opts?: {
8
10
  days?: number;
9
11
  ids?: RecipeId[];
10
- }): TrendsDashboard;
11
- export { trendsWindow, runRecipe, RECIPE_IDS, type RecipeId, type RecipeSection };
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, trendsWindow, } from './recipes.js';
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 buildTrendsDashboard(opts = {}) {
12
+ export function buildMixDashboard(opts = {}) {
13
13
  const t0 = Date.now();
14
- const win = trendsWindow(opts.days ?? 7);
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
- export { trendsWindow, runRecipe, RECIPE_IDS };
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
- export interface TrendsWindow {
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 trendsWindow(days: number): TrendsWindow;
14
- export declare function recipeHarnessMix(win: TrendsWindow): RecipeSection;
15
- export declare function recipeModelMix(win: TrendsWindow): RecipeSection;
16
- export declare function recipeToolsPerSession(win: TrendsWindow): RecipeSection;
17
- export declare function recipeTokenRatio(win: TrendsWindow): RecipeSection;
18
- export declare function recipeSessionVolume(win: TrendsWindow): RecipeSection;
19
- export declare function recipeSecretsHot(win: TrendsWindow): RecipeSection;
20
- export declare function recipeBrowserActivity(win: TrendsWindow): RecipeSection;
21
- export declare function recipeResourceMix(win: TrendsWindow): RecipeSection;
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: TrendsWindow): RecipeSection;
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: TrendsWindow): boolean;
31
- export declare function usageKindsPresent(win: TrendsWindow): UsageKind[];
32
- export declare function usageEventCount(kind: UsageKind, win: TrendsWindow): number;
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 trendsWindow(days) {
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 = trendsWindow(7);
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>;
@@ -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
- if (!(await isDaemonReachable())) {
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>;