@my-life-buddies/cli 0.24.1 → 0.25.0

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.
@@ -178,6 +178,495 @@ ${line}`;
178
178
  }
179
179
  };
180
180
 
181
+ // apps/preview/src/project-inspection.ts
182
+ import { readFileSync, realpathSync, statSync } from "node:fs";
183
+ import { createRequire } from "node:module";
184
+ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
185
+ import { parse } from "acorn";
186
+ var Unknown = class {
187
+ constructor(reason = "\u914D\u7F6E\u4F9D\u8D56\u8FD0\u884C\u65F6\u4FE1\u606F\u6216\u6682\u4E0D\u652F\u6301\u7684\u8868\u8FBE\u5F0F\u3002", textPrefix) {
188
+ this.reason = reason;
189
+ this.textPrefix = textPrefix;
190
+ }
191
+ reason;
192
+ textPrefix;
193
+ };
194
+ var Api = class {
195
+ constructor(name) {
196
+ this.name = name;
197
+ }
198
+ name;
199
+ };
200
+ var PlatformTool = class {
201
+ constructor(name) {
202
+ this.name = name;
203
+ }
204
+ name;
205
+ };
206
+ var Fn = class {
207
+ constructor(node, scope) {
208
+ this.node = node;
209
+ this.scope = scope;
210
+ }
211
+ node;
212
+ scope;
213
+ };
214
+ var Fields = class _Fields {
215
+ constructor(entries, optional = false) {
216
+ this.entries = entries;
217
+ this.optional = optional;
218
+ }
219
+ entries;
220
+ optional;
221
+ get(key) {
222
+ for (let i = this.entries.length - 1; i >= 0; i--) {
223
+ const entry = this.entries[i];
224
+ if (entry.key !== void 0) {
225
+ if (entry.key === key) return entry.value();
226
+ } else {
227
+ const spread = entry.value();
228
+ if (!(spread instanceof _Fields)) return new Unknown("\u5BF9\u8C61\u5305\u542B\u52A8\u6001\u5C55\u5F00\uFF0C\u65E0\u6CD5\u786E\u5B9A\u6700\u7EC8\u914D\u7F6E\u3002");
229
+ const keys = spread.keys();
230
+ if (!keys || keys.includes(key)) return spread.get(key);
231
+ }
232
+ }
233
+ return void 0;
234
+ }
235
+ keys() {
236
+ const keys = /* @__PURE__ */ new Set();
237
+ for (const entry of this.entries) {
238
+ if (entry.key !== void 0) keys.add(entry.key);
239
+ else {
240
+ const spread = entry.value(), nested = spread instanceof _Fields ? spread.keys() : void 0;
241
+ if (!nested) return void 0;
242
+ nested.forEach((key) => keys.add(key));
243
+ }
244
+ }
245
+ return [...keys];
246
+ }
247
+ };
248
+ var fields = (data) => new Fields(Object.entries(data).map(([key, value]) => ({ key, value: () => value })));
249
+ var unknown = () => new Unknown();
250
+ var isPrimitive = (value) => value === null || value === void 0 || ["string", "number", "boolean"].includes(typeof value);
251
+ var Scope = class {
252
+ constructor(file, parent) {
253
+ this.file = file;
254
+ this.parent = parent;
255
+ }
256
+ file;
257
+ parent;
258
+ bindings = /* @__PURE__ */ new Map();
259
+ mutable = /* @__PURE__ */ new Set();
260
+ get(name) {
261
+ if (this.mutable.has(name)) return new Unknown("\u914D\u7F6E\u5305\u542B\u53EF\u53D8\u503C\u6216\u540E\u7EED\u4FEE\u6539\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002");
262
+ const binding = this.bindings.get(name);
263
+ return binding ? binding() : this.parent?.get(name) ?? new Unknown();
264
+ }
265
+ };
266
+ var within = (root, file) => {
267
+ const path = relative(root, file);
268
+ return !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`);
269
+ };
270
+ var keyOf = (node, computed = false) => !computed && node.type === "Identifier" ? node.name : node.type === "Literal" && ["string", "number"].includes(typeof node.value) ? String(node.value) : void 0;
271
+ var unwrap = (node) => node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration" ? node.declaration ?? node : node;
272
+ function walk(node, visit, functions = false) {
273
+ visit(node);
274
+ if (!functions && /^(?:Function|ArrowFunction)/u.test(node.type)) return;
275
+ for (const value of Object.values(node)) {
276
+ if (Array.isArray(value)) {
277
+ for (const child of value) if (child?.type) walk(child, visit, functions);
278
+ } else if (value && typeof value === "object" && value.type) walk(value, visit, functions);
279
+ }
280
+ }
281
+ var Reader = class {
282
+ sources = /* @__PURE__ */ new Map();
283
+ dependencies = /* @__PURE__ */ new Map();
284
+ root;
285
+ runtimeRoot;
286
+ bytes = 0;
287
+ steps = 0;
288
+ depth = 0;
289
+ constructor(root) {
290
+ this.root = realpathSync(root);
291
+ }
292
+ tick() {
293
+ if (++this.steps > 5e4) throw new Error("inspection budget");
294
+ }
295
+ lazy(read) {
296
+ let state = 0, result;
297
+ return () => {
298
+ this.tick();
299
+ if (state === 1) return new Unknown("\u914D\u7F6E\u5B58\u5728\u5FAA\u73AF\u5F15\u7528\u3002");
300
+ if (state === 2) return result;
301
+ state = 1;
302
+ try {
303
+ result = read();
304
+ } catch {
305
+ result = new Unknown("\u914D\u7F6E\u65E0\u6CD5\u9759\u6001\u8BFB\u53D6\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002");
306
+ }
307
+ state = 2;
308
+ return result;
309
+ };
310
+ }
311
+ source(path) {
312
+ if (!this.dependencies.has(path)) this.dependencies.set(path, fingerprint(path));
313
+ const file = realpathSync(path);
314
+ const inRuntime = this.runtimeRoot && within(this.runtimeRoot, file);
315
+ if ((!within(this.root, file) || relative(this.root, file).split(sep).includes("node_modules")) && !inRuntime) throw new Error("outside project");
316
+ if (![".js", ".mjs", ".cjs"].includes(extname(file))) throw new Error("unsupported source");
317
+ const existing = this.sources.get(file);
318
+ if (existing) return existing;
319
+ const size = statSync(file).size;
320
+ if (this.sources.size >= 80 || size > 512e3 || (this.bytes += size) > 3e6) throw new Error("inspection size");
321
+ const tree = parse(readFileSync(file, "utf8"), { ecmaVersion: "latest", sourceType: "module" });
322
+ const scope = new Scope(file), exports = /* @__PURE__ */ new Map();
323
+ const result = { tree, scope, exports };
324
+ this.sources.set(file, result);
325
+ this.bind(tree.body, scope);
326
+ for (const statement of tree.body) {
327
+ if (statement.type === "ImportDeclaration") {
328
+ for (const specifier of statement.specifiers) {
329
+ const name = specifier.type === "ImportDefaultSpecifier" ? "default" : specifier.type === "ImportNamespaceSpecifier" ? "*" : keyOf(specifier.imported);
330
+ scope.bindings.set(specifier.local.name, this.lazy(() => this.imported(scope.file, statement.source.value, name ?? "")));
331
+ }
332
+ } else if (statement.type === "ExportDefaultDeclaration") {
333
+ exports.set("default", this.lazy(() => this.expression(statement.declaration, scope)));
334
+ } else if (statement.type === "ExportNamedDeclaration") {
335
+ const declaration = statement.declaration;
336
+ if (declaration?.type === "FunctionDeclaration") exports.set(declaration.id.name, () => scope.get(declaration.id.name));
337
+ if (declaration?.type === "VariableDeclaration") {
338
+ for (const item of declaration.declarations) if (item.id.type === "Identifier") exports.set(item.id.name, () => scope.get(item.id.name));
339
+ }
340
+ for (const specifier of statement.specifiers) {
341
+ const name = keyOf(specifier.exported), local = keyOf(specifier.local);
342
+ exports.set(name, this.lazy(() => statement.source ? this.imported(file, statement.source.value, local) : scope.get(local)));
343
+ }
344
+ }
345
+ }
346
+ return result;
347
+ }
348
+ imported(file, specifier, name) {
349
+ if (["typebox", "@sinclair/typebox"].includes(specifier) && name === "Type") return new Api("Type");
350
+ if (specifier === "@my-life-buddies/buddy-runtime" && name === "BuddyServer") return new Api("BuddyServer");
351
+ if (!specifier.startsWith("./") && !specifier.startsWith("../")) return new Unknown("\u914D\u7F6E\u6765\u81EA\u5916\u90E8\u4F9D\u8D56\uFF0C\u65E0\u6CD5\u9759\u6001\u786E\u5B9A\u3002");
352
+ const target = resolve(dirname(file), specifier);
353
+ for (const candidate of [target, `${target}.mjs`, `${target}.js`, join(target, "index.js")]) {
354
+ try {
355
+ const source = this.source(candidate);
356
+ return name === "*" ? new Fields([...source.exports].map(([key, value]) => ({ key, value }))) : source.exports.get(name)?.() ?? unknown();
357
+ } catch {
358
+ }
359
+ }
360
+ return new Unknown("\u5F15\u7528\u7684\u672C\u5730\u6E90\u6587\u4EF6\u65E0\u6CD5\u8BFB\u53D6\u6216\u4E0D\u652F\u6301\u9759\u6001\u5206\u6790\u3002");
361
+ }
362
+ bind(statements, scope) {
363
+ const aliases = /* @__PURE__ */ new Map();
364
+ const localValues = /* @__PURE__ */ new Set();
365
+ for (const item of statements) {
366
+ const statement = unwrap(item);
367
+ if (statement.type === "FunctionDeclaration" && statement.id) scope.bindings.set(statement.id.name, () => new Fn(statement, scope));
368
+ if (statement.type === "VariableDeclaration") for (const declaration of statement.declarations) {
369
+ this.pattern(declaration.id, this.lazy(() => this.expression(declaration.init, scope)), scope);
370
+ if (declaration.id.type === "Identifier") localValues.add(declaration.id.name);
371
+ let source = declaration.init;
372
+ while (source?.type === "MemberExpression") source = source.object;
373
+ if (declaration.id.type === "Identifier" && source?.type === "Identifier") aliases.set(declaration.id.name, source.name);
374
+ if (statement.kind !== "const") walk(declaration.id, (node) => {
375
+ if (node.type === "Identifier") scope.mutable.add(node.name);
376
+ });
377
+ }
378
+ }
379
+ for (const item of statements) walk(item, (node) => {
380
+ if (node.type === "ExpressionStatement" && node.expression.type === "CallExpression") {
381
+ for (const argument of node.expression.arguments) {
382
+ let root = argument;
383
+ while (root.type === "MemberExpression") root = root.object;
384
+ if (root.type === "Identifier" && localValues.has(root.name)) scope.mutable.add(root.name);
385
+ }
386
+ }
387
+ let target;
388
+ if (node.type === "AssignmentExpression") target = node.left;
389
+ if (node.type === "UpdateExpression") target = node.argument;
390
+ if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && ["push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "copyWithin", "assign", "defineProperty"].includes(keyOf(node.callee.property, node.callee.computed) ?? "")) target = node.callee.object.name === "Object" ? node.arguments[0] : node.callee.object;
391
+ while (target?.type === "MemberExpression") target = target.object;
392
+ if (target?.type === "Identifier") scope.mutable.add(target.name);
393
+ });
394
+ for (let changed = true; changed; ) {
395
+ changed = false;
396
+ for (const [alias, original] of aliases) if (scope.mutable.has(alias) && !scope.mutable.has(original)) {
397
+ scope.mutable.add(original);
398
+ changed = true;
399
+ }
400
+ }
401
+ }
402
+ pattern(pattern, value, scope) {
403
+ if (pattern.type === "Identifier") scope.bindings.set(pattern.name, value);
404
+ else if (pattern.type === "ObjectPattern") for (const property of pattern.properties) {
405
+ if (property.type !== "Property") continue;
406
+ const key = keyOf(property.key, property.computed);
407
+ this.pattern(property.value, this.lazy(() => key === void 0 ? unknown() : this.member(value(), key)), scope);
408
+ }
409
+ else if (pattern.type === "ArrayPattern") pattern.elements.forEach((item, index) => {
410
+ if (item) this.pattern(item, this.lazy(() => this.member(value(), String(index))), scope);
411
+ });
412
+ else if (pattern.type === "AssignmentPattern") this.pattern(pattern.left, this.lazy(() => {
413
+ const item = value();
414
+ return item === void 0 ? this.expression(pattern.right, scope) : item;
415
+ }), scope);
416
+ }
417
+ functionScope(fn, args) {
418
+ const scope = new Scope(fn.scope.file, fn.scope);
419
+ fn.node.params.forEach((param, index) => this.pattern(param, () => args[index], scope));
420
+ if (fn.node.body.type === "BlockStatement") this.bind(fn.node.body.body, scope);
421
+ return scope;
422
+ }
423
+ call(fn, args) {
424
+ if (fn instanceof Api && fn.name.startsWith("Type.")) return this.schema(fn.name.slice(5), args);
425
+ if (!(fn instanceof Fn)) return new Unknown("\u51FD\u6570\u8C03\u7528\u7684\u7ED3\u679C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002");
426
+ if (++this.depth > 32) {
427
+ this.depth--;
428
+ return unknown();
429
+ }
430
+ try {
431
+ const scope = this.functionScope(fn, args), body = fn.node.body;
432
+ if (body.type !== "BlockStatement") return this.expression(body, scope);
433
+ for (const statement of body.body) {
434
+ if (statement.type === "ReturnStatement") return this.expression(statement.argument, scope);
435
+ let conditionalReturn = false;
436
+ walk(statement, (node) => {
437
+ if (node.type === "ReturnStatement") conditionalReturn = true;
438
+ });
439
+ if (conditionalReturn) return new Unknown("\u914D\u7F6E\u5DE5\u5382\u5305\u542B\u6761\u4EF6\u8FD4\u56DE\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002");
440
+ }
441
+ return unknown();
442
+ } finally {
443
+ this.depth--;
444
+ }
445
+ }
446
+ member(object3, key) {
447
+ if (object3 instanceof Fields) return object3.get(key);
448
+ if (object3 instanceof Api) return object3.name === "conversation.tools" ? new PlatformTool(key) : new Api(`${object3.name}.${key}`);
449
+ if (Array.isArray(object3)) return key === "length" ? object3.length : /^\d+$/u.test(key) ? object3[Number(key)] : unknown();
450
+ return object3 instanceof Unknown ? object3 : unknown();
451
+ }
452
+ expression(node, scope) {
453
+ this.tick();
454
+ if (!node) return void 0;
455
+ switch (node.type) {
456
+ case "Literal":
457
+ return node.regex || node.bigint ? unknown() : node.value;
458
+ case "Identifier":
459
+ return node.name === "undefined" ? void 0 : scope.get(node.name);
460
+ case "FunctionDeclaration":
461
+ case "FunctionExpression":
462
+ case "ArrowFunctionExpression":
463
+ return new Fn(node, scope);
464
+ case "AwaitExpression":
465
+ case "ChainExpression":
466
+ return this.expression(node.argument ?? node.expression, scope);
467
+ case "ObjectExpression":
468
+ return new Fields(node.properties.map((property) => ({ key: property.type === "SpreadElement" ? void 0 : keyOf(property.key, property.computed), value: this.lazy(() => property.type === "SpreadElement" ? this.expression(property.argument, scope) : property.kind === "get" || property.kind === "set" ? unknown() : property.computed && keyOf(property.key, true) === void 0 ? unknown() : this.expression(property.value, scope)) })));
469
+ case "ArrayExpression": {
470
+ const values = [];
471
+ for (const element of node.elements) {
472
+ if (element?.type === "SpreadElement") {
473
+ const value = this.expression(element.argument, scope);
474
+ values.push(...Array.isArray(value) ? value : [value instanceof Unknown ? value : unknown()]);
475
+ } else values.push(this.expression(element, scope));
476
+ }
477
+ return values;
478
+ }
479
+ case "MemberExpression": {
480
+ const key = node.computed ? this.expression(node.property, scope) : node.property.name;
481
+ return typeof key === "string" || typeof key === "number" ? this.member(this.expression(node.object, scope), String(key)) : unknown();
482
+ }
483
+ case "CallExpression": {
484
+ if (node.callee.type === "MemberExpression" && keyOf(node.callee.property, node.callee.computed) === "map") {
485
+ const values = this.expression(node.callee.object, scope), fn = this.expression(node.arguments[0], scope);
486
+ return Array.isArray(values) ? values.map((value, index) => this.call(fn, [value, index])) : unknown();
487
+ }
488
+ return this.call(this.expression(node.callee, scope), node.arguments.map((arg) => this.expression(arg, scope)));
489
+ }
490
+ case "ConditionalExpression": {
491
+ const test = this.expression(node.test, scope);
492
+ return isPrimitive(test) ? this.expression(test ? node.consequent : node.alternate, scope) : new Unknown("\u914D\u7F6E\u6839\u636E\u73AF\u5883\u6216\u4F1A\u8BDD\u52A8\u6001\u9009\u62E9\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002");
493
+ }
494
+ case "LogicalExpression": {
495
+ const left = this.expression(node.left, scope);
496
+ if (left instanceof Unknown) return left;
497
+ if (node.operator === "??") return left == null ? this.expression(node.right, scope) : left;
498
+ return (node.operator === "&&" ? Boolean(left) : !left) ? this.expression(node.right, scope) : left;
499
+ }
500
+ case "BinaryExpression": {
501
+ const left = this.expression(node.left, scope), right = this.expression(node.right, scope);
502
+ if (!isPrimitive(left) || !isPrimitive(right)) return node.operator === "+" && typeof left === "string" ? new Unknown("\u63CF\u8FF0\u5305\u542B\u52A8\u6001\u5185\u5BB9\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002", left) : unknown();
503
+ if (node.operator === "+" && (typeof left === "string" || typeof right === "string")) return `${left}${right}`;
504
+ if (node.operator === "===") return left === right;
505
+ if (node.operator === "!==") return left !== right;
506
+ return unknown();
507
+ }
508
+ case "UnaryExpression": {
509
+ const value = this.expression(node.argument, scope);
510
+ if (!isPrimitive(value)) return unknown();
511
+ return node.operator === "!" ? !value : node.operator === "-" && typeof value === "number" ? -value : unknown();
512
+ }
513
+ case "TemplateLiteral": {
514
+ let result = "";
515
+ for (let i = 0; i < node.quasis.length; i++) {
516
+ result += node.quasis[i].value.cooked;
517
+ if (i < node.expressions.length) {
518
+ const value = this.expression(node.expressions[i], scope);
519
+ if (!isPrimitive(value)) return new Unknown("\u63CF\u8FF0\u5305\u542B\u52A8\u6001\u5185\u5BB9\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002", result);
520
+ result += String(value);
521
+ }
522
+ }
523
+ return result;
524
+ }
525
+ default:
526
+ return unknown();
527
+ }
528
+ }
529
+ schema(name, args) {
530
+ const first = args[0];
531
+ if (name === "Optional") return first instanceof Fields ? new Fields(first.entries, true) : unknown();
532
+ const scalar = { String: "string", Number: "number", Integer: "integer", Boolean: "boolean", Null: "null" };
533
+ let schema, options;
534
+ if (scalar[name]) {
535
+ schema = fields({ type: scalar[name] });
536
+ options = first;
537
+ } else if (name === "Object" && first instanceof Fields) {
538
+ const keys = first.keys();
539
+ if (!keys) return unknown();
540
+ const required = [];
541
+ for (const key of keys) {
542
+ const value = first.get(key);
543
+ if (!(value instanceof Fields)) return unknown();
544
+ if (!value.optional) required.push(key);
545
+ }
546
+ schema = fields({ type: "object", properties: first, ...required.length ? { required } : {} });
547
+ options = args[1];
548
+ } else if (name === "Array") {
549
+ schema = fields({ type: "array", items: first });
550
+ options = args[1];
551
+ } else if (name === "Literal" && isPrimitive(first) && first !== void 0) {
552
+ schema = fields({ const: first, type: first === null ? "null" : typeof first });
553
+ options = args[1];
554
+ } else if (name === "Union" && Array.isArray(first)) {
555
+ schema = first.length === 1 && first[0] instanceof Fields ? first[0] : fields({ anyOf: first });
556
+ options = args[1];
557
+ } else if (name === "Any" || name === "Unknown") {
558
+ schema = fields({});
559
+ options = first;
560
+ } else return new Unknown("\u53C2\u6570\u4F7F\u7528\u6682\u4E0D\u652F\u6301\u7684 Schema \u6784\u9020\u65B9\u6CD5\u3002");
561
+ return options === void 0 ? schema : options instanceof Fields ? new Fields([...schema.entries, { value: () => options }]) : unknown();
562
+ }
563
+ json(value, depth = 0) {
564
+ this.tick();
565
+ if (depth > 30) throw new Error("schema depth");
566
+ if (isPrimitive(value)) return value;
567
+ if (Array.isArray(value)) return value.map((item) => this.json(item, depth + 1));
568
+ if (value instanceof Fields) {
569
+ const keys = value.keys();
570
+ if (!keys) throw new Error("dynamic schema");
571
+ return Object.fromEntries(keys.map((key) => [key, this.json(value.get(key), depth + 1)]));
572
+ }
573
+ throw new Error("dynamic schema");
574
+ }
575
+ runtime() {
576
+ try {
577
+ const path = createRequire(join(this.root, "package.json")).resolve("@my-life-buddies/buddy-runtime/package.json");
578
+ this.dependencies.set(path, fingerprint(path));
579
+ const manifest = JSON.parse(readFileSync(path, "utf8"));
580
+ if (manifest.name !== "@my-life-buddies/buddy-runtime") throw new Error("runtime package");
581
+ this.runtimeRoot = realpathSync(dirname(path));
582
+ const source = this.source(join(this.runtimeRoot, "dist/conversation/agent.js"));
583
+ const factory = source.exports.get("createAgent")?.();
584
+ const scope = factory instanceof Fn ? this.functionScope(factory, [unknown()]) : void 0;
585
+ return { tools: scope?.get("platformTools") ?? unknown(), builtin: source.exports.has("builtinTools") };
586
+ } catch {
587
+ return { tools: unknown(), builtin: true };
588
+ }
589
+ }
590
+ inspect() {
591
+ const source = this.source(join(this.root, "start.mjs"));
592
+ const starts = [];
593
+ walk(source.tree, (node) => {
594
+ if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
595
+ if (keyOf(node.callee.property, node.callee.computed) !== "start") return;
596
+ const owner = this.expression(node.callee.object, source.scope);
597
+ if (owner instanceof Api && owner.name === "BuddyServer") starts.push(node);
598
+ });
599
+ const conversation = new Fields([{ value: () => new Unknown("\u914D\u7F6E\u4F9D\u8D56\u5F53\u524D\u4F1A\u8BDD\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002") }, { key: "tools", value: () => new Api("conversation.tools") }]);
600
+ const options = starts.length === 1 ? this.call(this.expression(starts[0].arguments[0], source.scope), [conversation]) : new Unknown("\u672A\u8BC6\u522B\u5230\u552F\u4E00\u7684 BuddyServer.start \u5DE5\u5382\u5165\u53E3\u3002");
601
+ const initial = this.member(options, "initialState"), model = this.member(initial, "model");
602
+ const toolValues = this.member(initial, "tools"), runtime = this.runtime();
603
+ const tools = [], unresolved = [];
604
+ const list = toolValues === void 0 && initial instanceof Fields ? [] : Array.isArray(toolValues) ? toolValues : [toolValues];
605
+ let payloadSize = 0;
606
+ for (const item of list) {
607
+ if (tools.length + unresolved.length >= 128 || payloadSize > 25e4) {
608
+ unresolved.push({ reason: "\u914D\u7F6E\u6E05\u5355\u8D85\u51FA\u9759\u6001\u5C55\u793A\u4E0A\u9650\uFF0C\u5176\u4F59\u5DE5\u5177\u8BF7\u5728\u6A21\u578B\u8BF7\u6C42\u4E2D\u67E5\u770B\u3002" });
609
+ break;
610
+ }
611
+ const origin = item instanceof PlatformTool ? "platform" : "custom";
612
+ const value = item instanceof PlatformTool ? this.member(runtime.tools, item.name) : item;
613
+ if (item instanceof PlatformTool && value === void 0) {
614
+ unresolved.push({ name: item.name, reason: "\u5F53\u524D\u5B89\u88C5\u7684 Runtime \u672A\u63D0\u4F9B\u6B64\u5E73\u53F0\u5DE5\u5177\uFF0C\u8BF7\u68C0\u67E5\u6CE8\u518C\u914D\u7F6E\u3002" });
615
+ continue;
616
+ }
617
+ const name = item instanceof PlatformTool ? item.name : this.member(value, "name");
618
+ if (typeof name !== "string" || !/^[\w.-]{1,128}$/u.test(name)) {
619
+ unresolved.push({ reason: value instanceof Unknown ? value.reason : "\u5DE5\u5177\u540D\u79F0\u6216\u6CE8\u518C\u5173\u7CFB\u65E0\u6CD5\u9759\u6001\u786E\u5B9A\u3002" });
620
+ continue;
621
+ }
622
+ const description = this.member(value, "description"), parameters = this.member(value, "parameters");
623
+ let schema;
624
+ try {
625
+ const json2 = this.json(parameters);
626
+ if (json2 && typeof json2 === "object" && !Array.isArray(json2) && JSON.stringify(json2).length <= 32768) schema = json2;
627
+ } catch {
628
+ }
629
+ const reason = [typeof description !== "string" ? description instanceof Unknown && description.textPrefix ? "\u63CF\u8FF0\u542B\u52A8\u6001\u5185\u5BB9\uFF0C\u5F53\u524D\u4EC5\u5C55\u793A\u53EF\u8BFB\u53D6\u90E8\u5206\u3002" : "\u5DE5\u5177\u63CF\u8FF0\u5C1A\u672A\u9759\u6001\u8BFB\u53D6\u3002" : "", !schema ? "\u53C2\u6570\u5B9A\u4E49\u5C1A\u672A\u9759\u6001\u8BFB\u53D6\u3002" : ""].join("");
630
+ const descriptionText = typeof description === "string" ? description : description instanceof Unknown ? description.textPrefix : void 0;
631
+ const tool = { name, origin, ...descriptionText ? { description: descriptionText.slice(0, 4096) } : {}, ...schema ? { parameters: schema } : {}, ...reason ? { reason } : {} };
632
+ payloadSize += Buffer.byteLength(JSON.stringify(tool));
633
+ tools.push(tool);
634
+ }
635
+ if (runtime.builtin) unresolved.push({ name: "Runtime \u5185\u7F6E\u5DE5\u5177", reason: "Runtime \u53EF\u80FD\u6309\u6A21\u578B\u80FD\u529B\u8FFD\u52A0\u5DE5\u5177\uFF1B\u662F\u5426\u542F\u7528\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\uFF0C\u53EF\u5728\u6A21\u578B\u8BF7\u6C42\u4E2D\u67E5\u770B\u3002" });
636
+ const seen = /* @__PURE__ */ new Set();
637
+ for (const tool of tools) {
638
+ if (seen.has(tool.name)) unresolved.push({ name: tool.name, reason: "\u6CE8\u518C\u5217\u8868\u4E2D\u5B58\u5728\u540C\u540D\u5DE5\u5177\uFF0C\u8BF7\u68C0\u67E5\u5DE5\u7A0B\u914D\u7F6E\u3002" });
639
+ seen.add(tool.name);
640
+ }
641
+ return { status: unresolved.length || tools.some((tool) => tool.reason) ? "partial" : "complete", model: typeof model === "string" && model.length > 0 && model.length <= 128 ? { status: "known", value: model } : { status: "dynamic", reason: model instanceof Unknown ? model.reason : "\u6A21\u578B\u914D\u7F6E\u65E0\u6CD5\u9759\u6001\u786E\u5B9A\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002" }, tools, unresolved };
642
+ }
643
+ };
644
+ var fingerprint = (path) => {
645
+ try {
646
+ const stat = statSync(path);
647
+ return `${realpathSync(path)}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
648
+ } catch {
649
+ return "missing";
650
+ }
651
+ };
652
+ function createProjectInspector(projectRoot) {
653
+ let cached;
654
+ let files = /* @__PURE__ */ new Map();
655
+ return async () => {
656
+ if (cached && [...files].every(([path, stamp]) => fingerprint(path) === stamp)) return cached;
657
+ let reader;
658
+ try {
659
+ reader = new Reader(projectRoot);
660
+ cached = reader.inspect();
661
+ } catch {
662
+ cached = { status: "partial", model: { status: "dynamic", reason: "\u5DE5\u7A0B\u5165\u53E3\u65E0\u6CD5\u9759\u6001\u8BFB\u53D6\uFF0C\u9700\u8FD0\u884C\u65F6\u786E\u8BA4\u3002" }, tools: [], unresolved: [{ reason: "\u5DE5\u5177\u6CE8\u518C\u5173\u7CFB\u5C1A\u672A\u8BFB\u53D6\uFF1B\u8BF7\u786E\u8BA4\u5165\u53E3\u53CA\u5F15\u7528\u6587\u4EF6\u5B58\u5728\u4E14\u8BED\u6CD5\u6709\u6548\u3002" }] };
663
+ }
664
+ files = new Map([join(projectRoot, "start.mjs"), join(projectRoot, "node_modules/@my-life-buddies/buddy-runtime/package.json")].map((path) => [path, fingerprint(path)]));
665
+ for (const [path, stamp] of reader?.dependencies ?? []) files.set(path, stamp);
666
+ return cached;
667
+ };
668
+ }
669
+
181
670
  // apps/preview/src/controller.ts
182
671
  var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
183
672
  function publicFailure(cause) {
@@ -193,6 +682,8 @@ var PreviewController = class {
193
682
  #readConfig;
194
683
  #readBuddyDetails;
195
684
  #now;
685
+ #readInspection;
686
+ #inspectionPromise;
196
687
  #listeners = /* @__PURE__ */ new Set();
197
688
  #simulatorListeners = /* @__PURE__ */ new Set();
198
689
  #runs = /* @__PURE__ */ new Map();
@@ -225,6 +716,7 @@ var PreviewController = class {
225
716
  this.#readConfig = options.readConfig ?? ((root) => readBuddyConfig(root));
226
717
  this.#readBuddyDetails = options.readBuddyDetails;
227
718
  this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
719
+ this.#readInspection = options.readInspection ?? createProjectInspector(options.projectRoot);
228
720
  }
229
721
  get projectRoot() {
230
722
  return this.#projectRoot;
@@ -232,6 +724,18 @@ var PreviewController = class {
232
724
  state() {
233
725
  return this.#state;
234
726
  }
727
+ async refreshInspection() {
728
+ if (this.#closed) return;
729
+ if (this.#inspectionPromise) return this.#inspectionPromise;
730
+ this.#inspectionPromise = this.#readInspection().then((inspection) => {
731
+ if (!this.#closed && JSON.stringify(inspection) !== JSON.stringify(this.#state.inspection)) {
732
+ this.#setState({ ...this.#state, inspection });
733
+ }
734
+ }).finally(() => {
735
+ this.#inspectionPromise = void 0;
736
+ });
737
+ return this.#inspectionPromise;
738
+ }
235
739
  async project() {
236
740
  return this.#readConfig(this.#projectRoot);
237
741
  }
@@ -275,6 +779,7 @@ var PreviewController = class {
275
779
  }
276
780
  this.#record({ channel: "dev", type: `dev.${phase}`, summary: phase === "reloading" ? "\u672C\u5730\u6587\u4EF6\u5DF2\u4FEE\u6539\uFF0C\u6B63\u5728\u91CD\u8F7D\uFF1B\u5F53\u524D\u56DE\u590D\u53EF\u80FD\u4E2D\u65AD" : phase === "reload_failed" ? error?.message ?? "\u91CD\u8F7D\u5931\u8D25\uFF0C\u4FEE\u6B63\u6587\u4EF6\u540E\u81EA\u52A8\u91CD\u8BD5" : "\u91CD\u8F7D\u5B8C\u6210\uFF0CDEV \u5DF2\u5C31\u7EEA" });
277
781
  this.#setState({ phase, ready: this.#state.ready, ...error ? { error } : {} });
782
+ void this.refreshInspection();
278
783
  },
279
784
  onLog: ({ source, text: text2 }) => {
280
785
  if (controller.signal.aborted) return;
@@ -835,7 +1340,7 @@ var PreviewController = class {
835
1340
  if (this.#events.length > 300) this.#events.splice(0, this.#events.length - 300);
836
1341
  }
837
1342
  #setState(next) {
838
- this.#state = Object.freeze({ ...next, revision: this.#state.revision + 1 });
1343
+ this.#state = Object.freeze({ ...this.#state.inspection ? { inspection: this.#state.inspection } : {}, ...next, revision: this.#state.revision + 1 });
839
1344
  for (const listener of this.#listeners) {
840
1345
  try {
841
1346
  listener(this.#state);
@@ -848,54 +1353,56 @@ var PreviewController = class {
848
1353
 
849
1354
  // apps/preview/src/server.ts
850
1355
  import { randomBytes, timingSafeEqual } from "node:crypto";
851
- import { readFileSync } from "node:fs";
1356
+ import { readFileSync as readFileSync2 } from "node:fs";
852
1357
  import { createServer } from "node:http";
853
- import { createRequire } from "node:module";
1358
+ import { createRequire as createRequire2 } from "node:module";
854
1359
  import QRCode from "qrcode";
855
1360
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "localhost"]);
856
1361
  var SESSION_COOKIE_PREFIX = "buddy_preview_session";
857
- var require2 = createRequire(import.meta.url);
1362
+ var require2 = createRequire2(import.meta.url);
858
1363
  var BRAND_ICON = Buffer.from(
859
- readFileSync(new URL("../web/assets/developer-platform-icon.png.base64", import.meta.url), "utf8").trim(),
1364
+ readFileSync2(new URL("../web/assets/developer-platform-icon.png.base64", import.meta.url), "utf8").trim(),
860
1365
  "base64"
861
1366
  );
862
1367
  var WEB_ASSETS = Object.freeze(/* @__PURE__ */ new Map([
863
1368
  ["/", {
864
1369
  contentType: "text/html; charset=utf-8",
865
- body: readFileSync(new URL("../web/index.html", import.meta.url))
1370
+ body: readFileSync2(new URL("../web/index.html", import.meta.url))
866
1371
  }],
867
1372
  ["/assets/app.css", {
868
1373
  contentType: "text/css; charset=utf-8",
869
- body: readFileSync(new URL("../web/app.css", import.meta.url))
1374
+ body: readFileSync2(new URL("../web/app.css", import.meta.url))
870
1375
  }],
871
1376
  ["/assets/tokens.css", {
872
1377
  contentType: "text/css; charset=utf-8",
873
- body: readFileSync(new URL("../web/tokens.css", import.meta.url))
1378
+ body: readFileSync2(new URL("../web/tokens.css", import.meta.url))
874
1379
  }],
875
1380
  ["/assets/brand.css", {
876
1381
  contentType: "text/css; charset=utf-8",
877
- body: readFileSync(new URL("../web/brand.css", import.meta.url))
1382
+ body: readFileSync2(new URL("../web/brand.css", import.meta.url))
878
1383
  }],
879
- ["/assets/widgets.js", { contentType: "text/javascript; charset=utf-8", body: readFileSync(new URL("../web/widgets.js", import.meta.url)) }],
880
- ["/assets/widgets.css", { contentType: "text/css; charset=utf-8", body: readFileSync(new URL("../web/widgets.css", import.meta.url)) }],
1384
+ ["/assets/widgets.js", { contentType: "text/javascript; charset=utf-8", body: readFileSync2(new URL("../web/widgets.js", import.meta.url)) }],
1385
+ ["/assets/widgets.css", { contentType: "text/css; charset=utf-8", body: readFileSync2(new URL("../web/widgets.css", import.meta.url)) }],
881
1386
  ["/assets/app.js", {
882
1387
  contentType: "text/javascript; charset=utf-8",
883
- body: readFileSync(new URL("../web/app.js", import.meta.url))
1388
+ body: readFileSync2(new URL("../web/app.js", import.meta.url))
884
1389
  }],
885
- ["/assets/diagnostics.js", { contentType: "text/javascript; charset=utf-8", body: readFileSync(new URL("../web/diagnostics.js", import.meta.url)) }],
1390
+ ["/assets/diagnostics.js", { contentType: "text/javascript; charset=utf-8", body: readFileSync2(new URL("../web/diagnostics.js", import.meta.url)) }],
1391
+ ["/assets/tool-inventory.js", { contentType: "text/javascript; charset=utf-8", body: readFileSync2(new URL("../web/tool-inventory.js", import.meta.url)) }],
1392
+ ["/assets/chat-markdown.js", { contentType: "text/javascript; charset=utf-8", body: readFileSync2(new URL("../web/chat-markdown.js", import.meta.url)) }],
886
1393
  ["/assets/developer-platform-icon.png", {
887
1394
  contentType: "image/png",
888
1395
  body: BRAND_ICON
889
1396
  }],
890
1397
  ["/assets/iphone-16-max.js", {
891
1398
  contentType: "text/javascript; charset=utf-8",
892
- body: readFileSync(require2.resolve("@sneas/telephone/iphone-16-max.js"))
1399
+ body: readFileSync2(require2.resolve("@sneas/telephone/iphone-16-max.js"))
893
1400
  }]
894
1401
  ]));
895
1402
  function closeServer(server) {
896
1403
  if (!server.listening) return Promise.resolve();
897
- return new Promise((resolve, reject) => {
898
- server.close((error) => error ? reject(error) : resolve());
1404
+ return new Promise((resolve2, reject) => {
1405
+ server.close((error) => error ? reject(error) : resolve2());
899
1406
  });
900
1407
  }
901
1408
  function writeHeaders(response) {
@@ -967,7 +1474,7 @@ function widgetDocument(response, html, render, state = "ready", status = 200) {
967
1474
  response.setHeader("Content-Security-Policy", "sandbox allow-scripts; default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; connect-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'");
968
1475
  response.setHeader("Content-Type", "text/html; charset=utf-8");
969
1476
  if (validWidgetRender(render)) {
970
- const script = state === "ready" ? readFileSync(new URL("../web/widget-document.js", import.meta.url), "utf8").replace("__WIDGET_RENDER__", JSON.stringify(render)) : `window.addEventListener('load',()=>parent.postMessage({channel:'buddy-widget-document',render:${JSON.stringify(render)},state:'error'},'*'),{once:true})`;
1477
+ const script = state === "ready" ? readFileSync2(new URL("../web/widget-document.js", import.meta.url), "utf8").replace("__WIDGET_RENDER__", JSON.stringify(render)) : `window.addEventListener('load',()=>parent.postMessage({channel:'buddy-widget-document',render:${JSON.stringify(render)},state:'error'},'*'),{once:true})`;
971
1478
  const signal = `<script>${script}</script>`;
972
1479
  if (/<!doctype[^>]*>/iu.test(html)) html = html.replace(/<!doctype[^>]*>/iu, (match) => match + signal);
973
1480
  else html = signal + html;
@@ -1082,12 +1589,14 @@ data: ${JSON.stringify(value)}
1082
1589
  }
1083
1590
  if (request.method === "GET" && url.pathname === "/api/project") {
1084
1591
  try {
1085
- return json(response, 200, { project: publicProject(await options.controller.project()) });
1592
+ await options.controller.refreshInspection();
1593
+ return json(response, 200, { project: { ...publicProject(await options.controller.project()), inspection: options.controller.state().inspection } });
1086
1594
  } catch {
1087
1595
  return apiError(response, 422, "PROJECT_CONFIG_INVALID", "\u9879\u76EE\u914D\u7F6E\u65E0\u6CD5\u8BFB\u53D6\u6216\u6821\u9A8C\u5931\u8D25");
1088
1596
  }
1089
1597
  }
1090
1598
  if (request.method === "GET" && url.pathname === "/api/dev") {
1599
+ await options.controller.refreshInspection();
1091
1600
  return json(response, 200, { state: options.controller.state() });
1092
1601
  }
1093
1602
  if (request.method === "GET" && url.pathname === "/api/buddy") {
@@ -1248,11 +1757,11 @@ data: ${JSON.stringify({
1248
1757
  return apiError(response, 404, "NOT_FOUND", "\u63A5\u53E3\u4E0D\u5B58\u5728");
1249
1758
  });
1250
1759
  try {
1251
- await new Promise((resolve, reject) => {
1760
+ await new Promise((resolve2, reject) => {
1252
1761
  server.once("error", reject);
1253
1762
  server.listen(port, host, () => {
1254
1763
  server.off("error", reject);
1255
- resolve();
1764
+ resolve2();
1256
1765
  });
1257
1766
  });
1258
1767
  const address = server.address();
@@ -1515,7 +2024,7 @@ async function mlbAppCommand(url, token, method, params, options = {}) {
1515
2024
  if (options.signal?.aborted) throw options.signal.reason;
1516
2025
  const requestId = randomUUID2();
1517
2026
  const socket = appSocket(url, token);
1518
- return await new Promise((resolve, reject) => {
2027
+ return await new Promise((resolve2, reject) => {
1519
2028
  let settled = false;
1520
2029
  let ready = false;
1521
2030
  let sent = false;
@@ -1528,7 +2037,7 @@ async function mlbAppCommand(url, token, method, params, options = {}) {
1528
2037
  if (settled) return;
1529
2038
  settled = true;
1530
2039
  cleanup();
1531
- resolve(value);
2040
+ resolve2(value);
1532
2041
  };
1533
2042
  const fail = (cause) => {
1534
2043
  if (settled) return;
@@ -1658,8 +2167,8 @@ async function* mlbAppEvents(url, token, options = {}) {
1658
2167
  queuedBytes -= item.bytes;
1659
2168
  yield item.frame;
1660
2169
  } else {
1661
- await new Promise((resolve) => {
1662
- wake = resolve;
2170
+ await new Promise((resolve2) => {
2171
+ wake = resolve2;
1663
2172
  });
1664
2173
  wake = void 0;
1665
2174
  }
@@ -1808,14 +2317,14 @@ function localReceipt(prefix, value) {
1808
2317
  }
1809
2318
  function waitForRetry(milliseconds, signal) {
1810
2319
  if (signal?.aborted) return Promise.reject(signal.reason);
1811
- return new Promise((resolve, reject) => {
2320
+ return new Promise((resolve2, reject) => {
1812
2321
  const abort = () => {
1813
2322
  clearTimeout(timer);
1814
2323
  reject(signal?.reason);
1815
2324
  };
1816
2325
  const timer = setTimeout(() => {
1817
2326
  signal?.removeEventListener("abort", abort);
1818
- resolve();
2327
+ resolve2();
1819
2328
  }, milliseconds);
1820
2329
  signal?.addEventListener("abort", abort, { once: true });
1821
2330
  });