@phi-code-admin/phi-code 0.75.7 → 0.76.1
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 +36 -0
- package/extensions/phi/README.md +26 -1
- package/extensions/phi/browser.ts +319 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.76.0] - 2026-05-16
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Bundled browser engine.** Phi-code now ships a fully self-hosted
|
|
8
|
+
Camoufox stack as new optional dependency `@phi-code-admin/browser` and
|
|
9
|
+
a new `browser.ts` extension that registers ten high-level tools:
|
|
10
|
+
`browser_navigate`, `browser_extract`, `browser_screenshot`,
|
|
11
|
+
`browser_search`, `browser_click`, `browser_type`, `browser_scroll`,
|
|
12
|
+
`browser_snapshot`, `browser_close_tab`, `browser_list_tabs`. Backed by
|
|
13
|
+
vendored snapshots of [apify/camoufox-js](https://github.com/apify/camoufox-js)
|
|
14
|
+
(MPL-2.0) and [jo-inc/camofox-browser](https://github.com/jo-inc/camofox-browser)
|
|
15
|
+
(MIT). Camoufox v135.0.1-beta.24 binaries are re-hosted on
|
|
16
|
+
[uglyswap/phi-code releases](https://github.com/uglyswap/phi-code/releases/tag/binaries-v1.0.0)
|
|
17
|
+
and downloaded once by the camoufox-js postinstall — no runtime call to
|
|
18
|
+
daijro/camoufox.
|
|
19
|
+
- New env vars: `PHI_BROWSER_DISABLED=1`, `CAMOUFOX_BIN_DIR=/path`,
|
|
20
|
+
`CAMOUFOX_SKIP_DOWNLOAD=1`, `CAMOUFOX_ALLOW_GITHUB_FETCH=1`,
|
|
21
|
+
`CAMOFOX_CRASH_REPORT_URL=https://your-relay` (crash telemetry is OFF
|
|
22
|
+
by default — see THIRD_PARTY_LICENSES.md).
|
|
23
|
+
|
|
24
|
+
## [0.75.7] - 2026-05-15
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Settings flush before process.exit so `/model` persists across sessions.
|
|
29
|
+
|
|
30
|
+
## [0.75.6] - 2026-05-15
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- `/model` is now the single source of truth for the chat default model.
|
|
35
|
+
The smart router no longer auto-switches per prompt (autoSwitch=off by
|
|
36
|
+
default; re-enable with `/routing autoswitch on`). `/phi-init` and
|
|
37
|
+
`/setup` no longer offer "Default model" — orchestration roles only.
|
|
38
|
+
|
|
3
39
|
## [Unreleased]
|
|
4
40
|
|
|
5
41
|
### Added
|
package/extensions/phi/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Phi Code Extensions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
11 TypeScript extensions automatically loaded at startup.
|
|
4
4
|
|
|
5
5
|
## Extensions
|
|
6
6
|
|
|
@@ -17,6 +17,31 @@
|
|
|
17
17
|
| **Setup** | `setup.ts` | — | `/setup` | — |
|
|
18
18
|
| **Keys** | `keys.ts` | — | `/keys` | `session_start` (hot-reload watcher) |
|
|
19
19
|
| **Models** | `models.ts` | — | `/models` | `session_start` (background catalog refresh) |
|
|
20
|
+
| **Browser** | `browser.ts` | `browser_navigate`, `browser_extract`, `browser_screenshot`, `browser_search`, `browser_click`, `browser_type`, `browser_scroll`, `browser_snapshot`, `browser_close_tab`, `browser_list_tabs` | — | `session_shutdown` (kill Firefox) |
|
|
21
|
+
|
|
22
|
+
## Bundled browser engine (Camoufox)
|
|
23
|
+
|
|
24
|
+
The `browser.ts` extension exposes ten high-level tools backed by a
|
|
25
|
+
vendored snapshot of [Camoufox](https://github.com/daijro/camoufox) v135.0.1-beta.24
|
|
26
|
+
(anti-detect Firefox fork, MPL-2.0). It bypasses Cloudflare and most
|
|
27
|
+
bot-detection that plain `fetch` + cheerio can't.
|
|
28
|
+
|
|
29
|
+
- Phi-code ships **its own copy** of the JS launcher and the OpenClaw
|
|
30
|
+
automation server (`@phi-code-admin/camoufox-js`,
|
|
31
|
+
`@phi-code-admin/camofox-browser`, `@phi-code-admin/browser`) so no
|
|
32
|
+
third-party-maintained npm package sits on the critical path.
|
|
33
|
+
- The Firefox binary itself is re-hosted on
|
|
34
|
+
[uglyswap/phi-code releases](https://github.com/uglyswap/phi-code/releases/tag/binaries-v1.0.0)
|
|
35
|
+
and downloaded once by the camoufox-js postinstall, cached under
|
|
36
|
+
`~/.cache/phi-code/camoufox/v1.0.0/<platform>-<arch>/` (XDG / Library /
|
|
37
|
+
LOCALAPPDATA respected). No runtime call to daijro/camoufox.
|
|
38
|
+
- `PHI_BROWSER_DISABLED=1` turns the extension off without uninstalling.
|
|
39
|
+
- `CAMOUFOX_BIN_DIR=/absolute/path` overrides the cache (air-gapped CI).
|
|
40
|
+
- `CAMOUFOX_SKIP_DOWNLOAD=1` skips the postinstall download; useful when
|
|
41
|
+
you want to ship pre-baked Docker images.
|
|
42
|
+
|
|
43
|
+
The web-search cascade (`web_search`, `/search`) is unchanged — Camoufox
|
|
44
|
+
is an additional capability, not a replacement.
|
|
20
45
|
|
|
21
46
|
## Live Model Catalogs
|
|
22
47
|
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Extension for Phi Code
|
|
3
|
+
*
|
|
4
|
+
* Registers 10 browser tools backed by the bundled Camoufox stack
|
|
5
|
+
* (`@phi-code-admin/browser`):
|
|
6
|
+
*
|
|
7
|
+
* browser_navigate — open/follow a URL
|
|
8
|
+
* browser_extract — readability extraction (works on SPAs)
|
|
9
|
+
* browser_screenshot — PNG capture, base64 in the tool result
|
|
10
|
+
* browser_search — DDG/Google search macro
|
|
11
|
+
* browser_click — click by accessibility ref or CSS selector
|
|
12
|
+
* browser_type — type text into focused/targeted element
|
|
13
|
+
* browser_scroll — page/element scroll
|
|
14
|
+
* browser_snapshot — accessibility tree with refs for follow-up tools
|
|
15
|
+
* browser_close_tab — release a single tab
|
|
16
|
+
* browser_list_tabs — list open tabs for the current session
|
|
17
|
+
*
|
|
18
|
+
* Lifecycle:
|
|
19
|
+
* - Lazy boot: the Camoufox server starts on the first tool call.
|
|
20
|
+
* - `session_shutdown`: best-effort `closeAll()` to avoid zombie Firefox.
|
|
21
|
+
* - PHI_BROWSER_DISABLED=1 disables the whole extension at startup (the
|
|
22
|
+
* user keeps the legacy `web_search` / `fetch_url` only).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createRequire } from "node:module";
|
|
26
|
+
import { existsSync } from "node:fs";
|
|
27
|
+
import { dirname, join } from "node:path";
|
|
28
|
+
import { pathToFileURL } from "node:url";
|
|
29
|
+
import { Type } from "@sinclair/typebox";
|
|
30
|
+
import type { ExtensionAPI } from "phi-code";
|
|
31
|
+
|
|
32
|
+
// PHI-VENDOR: dynamic import so phi-code keeps starting even when the
|
|
33
|
+
// vendored browser stack isn't installed (e.g. binaries unavailable for
|
|
34
|
+
// the host's `process.platform`-`process.arch` combo). We surface a
|
|
35
|
+
// concrete error on first tool call instead of refusing to boot.
|
|
36
|
+
type BrowserApi = typeof import("@phi-code-admin/browser");
|
|
37
|
+
|
|
38
|
+
let cachedApi: BrowserApi | undefined;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve `@phi-code-admin/browser` from the host phi-code installation
|
|
42
|
+
* (the binary that loaded us, via `process.argv[1]`), not from this file's
|
|
43
|
+
* location. The extension is typically copied by phi-code's postinstall
|
|
44
|
+
* into `~/.phi/agent/extensions/browser.ts`, which has no `node_modules`
|
|
45
|
+
* of its own — a plain `import("@phi-code-admin/browser")` would resolve
|
|
46
|
+
* relative to that copy and fail. Walking the resolution from
|
|
47
|
+
* `process.argv[1]` (the `phi` CLI entry, which DOES sit next to its
|
|
48
|
+
* bundled `node_modules`) finds the package every time.
|
|
49
|
+
*/
|
|
50
|
+
function browserPackageFromPhi(): string | undefined {
|
|
51
|
+
const cliPath = process.argv[1];
|
|
52
|
+
if (!cliPath) return undefined;
|
|
53
|
+
try {
|
|
54
|
+
const req = createRequire(pathToFileURL(cliPath));
|
|
55
|
+
return req.resolve("@phi-code-admin/browser");
|
|
56
|
+
} catch {
|
|
57
|
+
// Fall through — we'll try walking up from cliPath manually.
|
|
58
|
+
}
|
|
59
|
+
let dir = dirname(cliPath);
|
|
60
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
61
|
+
const candidate = join(dir, "node_modules", "@phi-code-admin", "browser", "dist", "index.js");
|
|
62
|
+
if (existsSync(candidate)) return candidate;
|
|
63
|
+
const parent = dirname(dir);
|
|
64
|
+
if (parent === dir) break;
|
|
65
|
+
dir = parent;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function getBrowserApi(): Promise<BrowserApi> {
|
|
71
|
+
if (cachedApi) return cachedApi;
|
|
72
|
+
|
|
73
|
+
// 1. Try the standard dynamic import first. This works when the extension
|
|
74
|
+
// lives next to a `node_modules/@phi-code-admin/browser` (dev / monorepo
|
|
75
|
+
// layouts and any setup where the user has run a fresh `npm install` in
|
|
76
|
+
// the extension's directory).
|
|
77
|
+
try {
|
|
78
|
+
cachedApi = (await import("@phi-code-admin/browser")) as BrowserApi;
|
|
79
|
+
return cachedApi;
|
|
80
|
+
} catch (firstErr) {
|
|
81
|
+
// 2. Fall back to resolving through the phi CLI binary, which always
|
|
82
|
+
// sits next to its bundled deps even when the extension was copied
|
|
83
|
+
// elsewhere by the postinstall script.
|
|
84
|
+
const resolved = browserPackageFromPhi();
|
|
85
|
+
if (resolved) {
|
|
86
|
+
try {
|
|
87
|
+
cachedApi = (await import(pathToFileURL(resolved).href)) as BrowserApi;
|
|
88
|
+
return cachedApi;
|
|
89
|
+
} catch (secondErr) {
|
|
90
|
+
// Re-throw the second error: it's the more informative one.
|
|
91
|
+
throw secondErr instanceof Error ? secondErr : new Error(String(secondErr));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// 3. No path worked. Throw the original error WITHOUT caching it, so
|
|
95
|
+
// the user can fix their install and the next tool call retries.
|
|
96
|
+
throw firstErr instanceof Error ? firstErr : new Error(String(firstErr));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isDisabled(): boolean {
|
|
101
|
+
const v = process.env.PHI_BROWSER_DISABLED;
|
|
102
|
+
return v === "1" || v === "true" || v === "yes";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function jsonResult(value: unknown): string {
|
|
106
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default function browserExtension(pi: ExtensionAPI) {
|
|
110
|
+
if (isDisabled()) {
|
|
111
|
+
// Keep startup quiet — the user opted out.
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── browser_navigate ─────────────────────────────────────────────
|
|
116
|
+
pi.registerTool({
|
|
117
|
+
name: "browser_navigate",
|
|
118
|
+
description:
|
|
119
|
+
"Open a URL in a Camoufox tab. If `tabId` is omitted, a new tab is created. Returns the tab id, the final URL and the HTTP status when available.",
|
|
120
|
+
parameters: Type.Object({
|
|
121
|
+
url: Type.String({ description: "Full URL (https://...)" }),
|
|
122
|
+
tabId: Type.Optional(Type.String()),
|
|
123
|
+
waitUntil: Type.Optional(
|
|
124
|
+
Type.Union([
|
|
125
|
+
Type.Literal("load"),
|
|
126
|
+
Type.Literal("domcontentloaded"),
|
|
127
|
+
Type.Literal("networkidle"),
|
|
128
|
+
]),
|
|
129
|
+
),
|
|
130
|
+
timeoutMs: Type.Optional(Type.Number()),
|
|
131
|
+
}),
|
|
132
|
+
execute: async (params) => {
|
|
133
|
+
const api = await getBrowserApi();
|
|
134
|
+
const res = await api.navigate(params);
|
|
135
|
+
return { content: [{ type: "text", text: jsonResult(res) }] };
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ─── browser_extract ──────────────────────────────────────────────
|
|
140
|
+
pi.registerTool({
|
|
141
|
+
name: "browser_extract",
|
|
142
|
+
description:
|
|
143
|
+
"Return the readable content of a page (Mozilla Readability under the hood). Works on SPA / JS-heavy sites. Pass either `tabId` (existing tab) or `url` (opens a fresh tab).",
|
|
144
|
+
parameters: Type.Object({
|
|
145
|
+
tabId: Type.Optional(Type.String()),
|
|
146
|
+
url: Type.Optional(Type.String()),
|
|
147
|
+
mode: Type.Optional(
|
|
148
|
+
Type.Union([
|
|
149
|
+
Type.Literal("readability"),
|
|
150
|
+
Type.Literal("html"),
|
|
151
|
+
Type.Literal("text"),
|
|
152
|
+
]),
|
|
153
|
+
),
|
|
154
|
+
}),
|
|
155
|
+
execute: async (params) => {
|
|
156
|
+
const api = await getBrowserApi();
|
|
157
|
+
const res = await api.extract(params);
|
|
158
|
+
return { content: [{ type: "text", text: jsonResult(res) }] };
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ─── browser_screenshot ───────────────────────────────────────────
|
|
163
|
+
pi.registerTool({
|
|
164
|
+
name: "browser_screenshot",
|
|
165
|
+
description:
|
|
166
|
+
"Capture a screenshot of the current tab as a PNG. The image bytes are returned base64-encoded under `bytesBase64`.",
|
|
167
|
+
parameters: Type.Object({
|
|
168
|
+
tabId: Type.String(),
|
|
169
|
+
fullPage: Type.Optional(Type.Boolean()),
|
|
170
|
+
}),
|
|
171
|
+
execute: async (params) => {
|
|
172
|
+
const api = await getBrowserApi();
|
|
173
|
+
const res = await api.screenshot(params);
|
|
174
|
+
return { content: [{ type: "text", text: jsonResult(res) }] };
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ─── browser_search ───────────────────────────────────────────────
|
|
179
|
+
pi.registerTool({
|
|
180
|
+
name: "browser_search",
|
|
181
|
+
description:
|
|
182
|
+
"Run a web search through the Camoufox browser (anti-detect Firefox) and return the readability extraction of the results page. Useful when scraping Google directly is rate-limited.",
|
|
183
|
+
parameters: Type.Object({
|
|
184
|
+
query: Type.String(),
|
|
185
|
+
engine: Type.Optional(
|
|
186
|
+
Type.Union([
|
|
187
|
+
Type.Literal("google"),
|
|
188
|
+
Type.Literal("duckduckgo"),
|
|
189
|
+
Type.Literal("bing"),
|
|
190
|
+
]),
|
|
191
|
+
),
|
|
192
|
+
}),
|
|
193
|
+
execute: async (params) => {
|
|
194
|
+
const api = await getBrowserApi();
|
|
195
|
+
const res = await api.search(params);
|
|
196
|
+
return { content: [{ type: "text", text: jsonResult(res) }] };
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ─── browser_click ────────────────────────────────────────────────
|
|
201
|
+
pi.registerTool({
|
|
202
|
+
name: "browser_click",
|
|
203
|
+
description:
|
|
204
|
+
"Click an element. Pass either `ref` (returned by browser_snapshot) or `selector` (CSS).",
|
|
205
|
+
parameters: Type.Object({
|
|
206
|
+
tabId: Type.String(),
|
|
207
|
+
ref: Type.Optional(Type.String()),
|
|
208
|
+
selector: Type.Optional(Type.String()),
|
|
209
|
+
button: Type.Optional(
|
|
210
|
+
Type.Union([
|
|
211
|
+
Type.Literal("left"),
|
|
212
|
+
Type.Literal("right"),
|
|
213
|
+
Type.Literal("middle"),
|
|
214
|
+
]),
|
|
215
|
+
),
|
|
216
|
+
}),
|
|
217
|
+
execute: async (params) => {
|
|
218
|
+
const api = await getBrowserApi();
|
|
219
|
+
const res = await api.click(params);
|
|
220
|
+
return { content: [{ type: "text", text: jsonResult(res) }] };
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ─── browser_type ─────────────────────────────────────────────────
|
|
225
|
+
pi.registerTool({
|
|
226
|
+
name: "browser_type",
|
|
227
|
+
description:
|
|
228
|
+
"Type text into an element. Pass `ref` or `selector` to target a specific input; otherwise types into the currently focused element. Set `pressEnter: true` to submit a form.",
|
|
229
|
+
parameters: Type.Object({
|
|
230
|
+
tabId: Type.String(),
|
|
231
|
+
text: Type.String(),
|
|
232
|
+
ref: Type.Optional(Type.String()),
|
|
233
|
+
selector: Type.Optional(Type.String()),
|
|
234
|
+
pressEnter: Type.Optional(Type.Boolean()),
|
|
235
|
+
delayMs: Type.Optional(Type.Number()),
|
|
236
|
+
}),
|
|
237
|
+
execute: async (params) => {
|
|
238
|
+
const api = await getBrowserApi();
|
|
239
|
+
const res = await api.type(params);
|
|
240
|
+
return { content: [{ type: "text", text: jsonResult(res) }] };
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ─── browser_scroll ───────────────────────────────────────────────
|
|
245
|
+
pi.registerTool({
|
|
246
|
+
name: "browser_scroll",
|
|
247
|
+
description:
|
|
248
|
+
"Scroll the page (or a specific element by `ref`) by `pixels` in the given direction.",
|
|
249
|
+
parameters: Type.Object({
|
|
250
|
+
tabId: Type.String(),
|
|
251
|
+
direction: Type.Union([
|
|
252
|
+
Type.Literal("up"),
|
|
253
|
+
Type.Literal("down"),
|
|
254
|
+
Type.Literal("left"),
|
|
255
|
+
Type.Literal("right"),
|
|
256
|
+
]),
|
|
257
|
+
ref: Type.Optional(Type.String()),
|
|
258
|
+
pixels: Type.Optional(Type.Number()),
|
|
259
|
+
}),
|
|
260
|
+
execute: async (params) => {
|
|
261
|
+
const api = await getBrowserApi();
|
|
262
|
+
const res = await api.scroll(params);
|
|
263
|
+
return { content: [{ type: "text", text: jsonResult(res) }] };
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ─── browser_snapshot ─────────────────────────────────────────────
|
|
268
|
+
pi.registerTool({
|
|
269
|
+
name: "browser_snapshot",
|
|
270
|
+
description:
|
|
271
|
+
"Return the accessibility tree for the current tab. Each node carries a `ref` that can be passed back to browser_click / browser_type / browser_scroll. Cheaper than parsing HTML.",
|
|
272
|
+
parameters: Type.Object({
|
|
273
|
+
tabId: Type.String(),
|
|
274
|
+
}),
|
|
275
|
+
execute: async (params) => {
|
|
276
|
+
const api = await getBrowserApi();
|
|
277
|
+
const res = await api.snapshot(params);
|
|
278
|
+
return { content: [{ type: "text", text: jsonResult(res) }] };
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ─── browser_close_tab ────────────────────────────────────────────
|
|
283
|
+
pi.registerTool({
|
|
284
|
+
name: "browser_close_tab",
|
|
285
|
+
description: "Close a single tab. The browser process stays warm.",
|
|
286
|
+
parameters: Type.Object({
|
|
287
|
+
tabId: Type.String(),
|
|
288
|
+
}),
|
|
289
|
+
execute: async (params) => {
|
|
290
|
+
const api = await getBrowserApi();
|
|
291
|
+
const res = await api.closeTab(params);
|
|
292
|
+
return { content: [{ type: "text", text: jsonResult(res) }] };
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ─── browser_list_tabs ────────────────────────────────────────────
|
|
297
|
+
pi.registerTool({
|
|
298
|
+
name: "browser_list_tabs",
|
|
299
|
+
description: "List open tabs for the current user.",
|
|
300
|
+
parameters: Type.Object({
|
|
301
|
+
userId: Type.Optional(Type.String()),
|
|
302
|
+
}),
|
|
303
|
+
execute: async (params) => {
|
|
304
|
+
const api = await getBrowserApi();
|
|
305
|
+
const res = await api.listTabs(params);
|
|
306
|
+
return { content: [{ type: "text", text: jsonResult(res) }] };
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ─── Lifecycle: shut the Firefox process down on session shutdown ──
|
|
311
|
+
pi.on("session_shutdown", async () => {
|
|
312
|
+
if (!cachedApi) return;
|
|
313
|
+
try {
|
|
314
|
+
await cachedApi.closeAll();
|
|
315
|
+
} catch {
|
|
316
|
+
// best-effort
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phi-code-admin/phi-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.1",
|
|
4
4
|
"description": "Coding agent CLI with persistent memory, sub-agents, intelligent routing, and orchestration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@mariozechner/jiti": "^2.6.5",
|
|
50
|
+
"@phi-code-admin/browser": "^1.0.1",
|
|
50
51
|
"@silvia-odwyer/photon-node": "^0.3.4",
|
|
51
52
|
"chalk": "^5.5.0",
|
|
52
53
|
"cli-highlight": "^2.1.11",
|