@hireobi/cli 0.1.8 → 0.1.10

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/dist/obi.js CHANGED
@@ -14618,6 +14618,7 @@ var optionDefinitions = {
14618
14618
  "field-path": { type: "string" },
14619
14619
  "segment-name": { type: "string" },
14620
14620
  text: { type: "string" },
14621
+ tag: { type: "string" },
14621
14622
  company: { type: "string" },
14622
14623
  "company-unknown": { type: "boolean" },
14623
14624
  product: { type: "string" },
@@ -14632,7 +14633,7 @@ var optionDefinitions = {
14632
14633
  help: { type: "boolean" },
14633
14634
  version: { type: "boolean" }
14634
14635
  };
14635
- var namedCommands = ["business", "capabilities", "credentials", "doctor", "eval", "harness", "login", "logout", "memory", "questions", "readiness", "research", "skill", "status", "strategies"];
14636
+ var namedCommands = ["business", "capabilities", "credentials", "doctor", "eval", "harness", "login", "logout", "memory", "questions", "readiness", "research", "skill", "status", "strategies", "support"];
14636
14637
  var memoryKinds = ["preference", "constraint", "decision"];
14637
14638
  var credentialsActions = ["list", "revoke"];
14638
14639
  var emailSchema = external_exports.email().max(254);
@@ -14651,6 +14652,8 @@ var CliError = class extends Error {
14651
14652
  command;
14652
14653
  authorization;
14653
14654
  account;
14655
+ /** The message `obi support` could not send, with the block to paste. */
14656
+ support;
14654
14657
  constructor(options) {
14655
14658
  super(options.message);
14656
14659
  this.name = "CliError";
@@ -14662,6 +14665,7 @@ var CliError = class extends Error {
14662
14665
  this.command = options.command;
14663
14666
  this.authorization = options.authorization;
14664
14667
  this.account = options.account;
14668
+ this.support = options.support;
14665
14669
  }
14666
14670
  };
14667
14671
  function usageError(message = "Invalid command usage.", options = {}) {
@@ -15174,6 +15178,52 @@ function parseMemoryArguments(positionals, values, context, common2) {
15174
15178
  throw invalid3();
15175
15179
  }
15176
15180
 
15181
+ // src/support-contract.ts
15182
+ var SUPPORT_EMAIL = "support@hireobi.com";
15183
+ var SUPPORT_TAGS = ["market-request", "scope-request"];
15184
+ var lastErrorSchema = external_exports.strictObject({
15185
+ code: external_exports.string().regex(/^[A-Z][A-Z0-9_]{0,79}$/),
15186
+ command: external_exports.string().regex(/^[a-z][a-z-]{0,39}$/),
15187
+ at: external_exports.iso.datetime({ offset: true })
15188
+ });
15189
+ var SupportMessageSchema = external_exports.strictObject({
15190
+ requestId: external_exports.string().regex(/^[0-9a-f]{32}$/),
15191
+ message: external_exports.string().trim().min(1).max(2e3),
15192
+ tag: external_exports.enum(SUPPORT_TAGS).nullable(),
15193
+ diagnostic: external_exports.strictObject({
15194
+ cliVersion: external_exports.string().min(1).max(40),
15195
+ node: external_exports.string().min(1).max(40),
15196
+ os: external_exports.string().min(1).max(120),
15197
+ lastError: lastErrorSchema.nullable()
15198
+ })
15199
+ });
15200
+ var SupportMessageResponseSchema = external_exports.object({
15201
+ schemaVersion: external_exports.literal(1),
15202
+ data: external_exports.object({ received: external_exports.literal(true) })
15203
+ });
15204
+ var supportDataSchema = external_exports.object({
15205
+ delivery: external_exports.enum(["sent", "not_sent"]),
15206
+ /** Why nothing was sent: the error code met, null once sent. */
15207
+ reason: external_exports.string().nullable(),
15208
+ supportEmail: external_exports.literal(SUPPORT_EMAIL),
15209
+ mailto: external_exports.string(),
15210
+ tag: external_exports.enum(SUPPORT_TAGS).nullable(),
15211
+ account: external_exports.string().nullable(),
15212
+ origin: external_exports.string().min(1),
15213
+ diagnosticText: external_exports.string()
15214
+ });
15215
+
15216
+ // src/support-arguments.ts
15217
+ function parseSupportArguments(positionals, values, context, common2) {
15218
+ const allowed = /* @__PURE__ */ new Set(["json", "timeout-ms", "tag"]);
15219
+ const message = positionals[1]?.trim() ?? "";
15220
+ const tag = values.tag;
15221
+ if (Object.entries(values).some(([key, value]) => value !== void 0 && value !== false && !allowed.has(key)) || positionals.length !== 2 || message.length === 0 || message.length > 2e3 || tag !== void 0 && !SUPPORT_TAGS.includes(tag)) {
15222
+ throw usageError(`Use support "<message of at most 2000 characters>" [--tag ${SUPPORT_TAGS.join("|")}].`, context);
15223
+ }
15224
+ return { command: "support", supportMessage: message, ...tag === void 0 ? {} : { supportTag: tag }, ...common2 };
15225
+ }
15226
+
15177
15227
  // src/research-arguments.ts
15178
15228
  function parseResearchScope(values, context) {
15179
15229
  const locale = values.locale;
@@ -15576,7 +15626,7 @@ function commandMode(command, testServer) {
15576
15626
  if (testServer) {
15577
15627
  return "test";
15578
15628
  }
15579
- return command === "login" || command === "logout" || command === "status" || command === "credentials" || command === "eval" || command === "harness" || command === "business" || command === "questions" || command === "strategies" || command === "readiness" || command === "memory" ? "product" : "local";
15629
+ return command === "login" || command === "logout" || command === "status" || command === "credentials" || command === "eval" || command === "harness" || command === "business" || command === "questions" || command === "strategies" || command === "readiness" || command === "memory" || command === "support" ? "product" : "local";
15580
15630
  }
15581
15631
  function errorContext(argv) {
15582
15632
  const usesTestServer = argv.some(
@@ -15762,6 +15812,8 @@ function parseCommandArguments(argv) {
15762
15812
  throw usageError("Use research crawl <domain> --checkpoint <file> --output <file> [--batch-size <1-100>] [--locale <tag>] [--pages priority|all] [--max-pages <n|none>] [--retry-failed|--refresh], research capture --input <selected-page-file> --checkpoint <file> --output <corpus-file>, research readiness --input <file> --output <file>, or research video <url> --output <directory> [--frames <0-30>].", context);
15763
15813
  }
15764
15814
  if (positionals[0] === "memory") return parseMemoryArguments(positionals, values, context, { evaluationCaseId, json: json2, timeoutMs });
15815
+ if (positionals[0] === "support") return parseSupportArguments(positionals, values, context, { json: json2, timeoutMs });
15816
+ if (values.tag !== void 0) throw usageError("--tag is only valid with obi support.", context);
15765
15817
  if (stage !== void 0 || input !== void 0 || output !== void 0 || checkpoint !== void 0 || batchSize !== void 0 || retryFailed || refresh || scopeFlagsUsed || values.frames !== void 0 || free || evidenceOptionsUsed) {
15766
15818
  throw usageError("--stage and --input are only valid with obi harness.", context);
15767
15819
  }
@@ -15856,9 +15908,6 @@ function parseCommandArguments(argv) {
15856
15908
  }
15857
15909
  if (command === "doctor") {
15858
15910
  if (evaluationCaseId !== void 0) throw usageError("Doctor cannot use --eval-case.", context);
15859
- if (timeoutValue !== void 0 && testServer === void 0) {
15860
- throw usageError("A timeout is only valid with a test server.", context);
15861
- }
15862
15911
  return { command, json: json2, testServer, timeoutMs };
15863
15912
  }
15864
15913
  if (command === "status") {
@@ -16587,14 +16636,78 @@ var BusinessItemReviewRequestSchema = external_exports.discriminatedUnion("decis
16587
16636
  })
16588
16637
  ]);
16589
16638
 
16639
+ // ../shared/lib/geo/markets.ts
16640
+ var SUPPORTED_MARKETS = [
16641
+ { iso: "US", name: "United States" },
16642
+ { iso: "CA", name: "Canada" },
16643
+ { iso: "GB", name: "United Kingdom" },
16644
+ { iso: "FR", name: "France" },
16645
+ { iso: "DE", name: "Germany" },
16646
+ { iso: "ES", name: "Spain" },
16647
+ { iso: "IT", name: "Italy" },
16648
+ { iso: "NL", name: "Netherlands" },
16649
+ { iso: "BE", name: "Belgium" },
16650
+ { iso: "CH", name: "Switzerland" },
16651
+ { iso: "AU", name: "Australia" },
16652
+ { iso: "SA", name: "Saudi Arabia" },
16653
+ { iso: "AE", name: "UAE" },
16654
+ { iso: "QA", name: "Qatar" },
16655
+ { iso: "KW", name: "Kuwait" }
16656
+ ];
16657
+ var BY_KEY = new Map(
16658
+ SUPPORTED_MARKETS.flatMap((market) => [
16659
+ [market.iso.toLowerCase(), market],
16660
+ [market.name.toLowerCase(), market]
16661
+ ])
16662
+ );
16663
+ var SUPPORTED_MARKET_NAMES = SUPPORTED_MARKETS.map(
16664
+ (market) => market.name
16665
+ );
16666
+
16667
+ // ../shared/lib/cli-version.ts
16668
+ var CLI_VERSION_HEADER = "x-obi-cli-version";
16669
+ var CLI_UPDATE_COMMAND = "npm i -g @hireobi/cli";
16670
+ var release = /^(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})\.(0|[1-9][0-9]{0,8})$/;
16671
+ function parseRelease(version3) {
16672
+ const match = release.exec(version3);
16673
+ return match === null ? null : [Number(match[1]), Number(match[2]), Number(match[3])];
16674
+ }
16675
+ function isOlderRelease(version3, other) {
16676
+ const left = parseRelease(version3);
16677
+ const right = parseRelease(other);
16678
+ if (left === null || right === null) return false;
16679
+ for (let index = 0; index < 3; index += 1) {
16680
+ if (left[index] !== right[index]) return left[index] < right[index];
16681
+ }
16682
+ return false;
16683
+ }
16684
+
16685
+ // src/version.ts
16686
+ var cliVersion = "0.1.10";
16687
+
16590
16688
  // src/product-api-failures.ts
16689
+ var versionHeaders = { [CLI_VERSION_HEADER]: cliVersion };
16690
+ function updateRequiredError() {
16691
+ return new CliError({
16692
+ code: "CLI_UPDATE_REQUIRED",
16693
+ message: `This Obi CLI (${cliVersion}) is too old for the Obi service.`,
16694
+ exitCode: 7,
16695
+ retryable: false,
16696
+ nextAction: `Run ${CLI_UPDATE_COMMAND}, then run obi once to refresh the Skill, and try again.`,
16697
+ mode: "product"
16698
+ });
16699
+ }
16700
+ function apiFailure(code, httpStatus, retryAfterSeconds) {
16701
+ if (code === "CLI_UPDATE_REQUIRED") throw updateRequiredError();
16702
+ return { ok: false, failure: { code, httpStatus, retryAfterSeconds } };
16703
+ }
16591
16704
  function transportError(code, message, exitCode, retryable) {
16592
16705
  return new CliError({
16593
16706
  code,
16594
16707
  message,
16595
16708
  exitCode,
16596
16709
  retryable,
16597
- nextAction: exitCode === 6 ? "Check the network connection and try again." : "Update the Obi CLI, then try again.",
16710
+ nextAction: exitCode === 6 ? `Check the network connection and try again. If it keeps failing, run obi support "<what happened>" or write to ${SUPPORT_EMAIL} with the command you ran and the error code ${code}.` : "Update the Obi CLI, then try again.",
16598
16711
  mode: "product"
16599
16712
  });
16600
16713
  }
@@ -16624,6 +16737,14 @@ function failureError(failure2, evaluationCaseId) {
16624
16737
  mode: "product",
16625
16738
  nextAction: `Run obi business show${scope} --json and read the current work before preparing a new result.`
16626
16739
  });
16740
+ if (failure2.code === "MARKET_UNSUPPORTED") return new CliError({
16741
+ code: failure2.code,
16742
+ message: "Obi does not measure this market yet.",
16743
+ exitCode: 5,
16744
+ retryable: false,
16745
+ mode: "product",
16746
+ nextAction: `Obi measures in: ${SUPPORTED_MARKET_NAMES.join(", ")}. Choose one of them with the customer, or record the request with obi support "<market and domain>" --tag market-request.`
16747
+ });
16627
16748
  if (failure2.code === "RATE_LIMITED") {
16628
16749
  const wait = failure2.retryAfterSeconds === null ? "a moment" : `${failure2.retryAfterSeconds} seconds`;
16629
16750
  return new CliError({
@@ -17209,11 +17330,13 @@ var pendingAuthorizationViewSchema = external_exports.object({
17209
17330
  var originMarker = {
17210
17331
  origin: external_exports.string().min(1).max(200).optional(),
17211
17332
  development: external_exports.literal(true).optional(),
17212
- evaluationCaseId: external_exports.uuid().optional()
17333
+ evaluationCaseId: external_exports.uuid().optional(),
17334
+ // Set when npm holds a newer CLI than this one (cli/src/update-check.ts).
17335
+ update: external_exports.object({ installed: external_exports.string().max(32), latest: external_exports.string().max(32), command: external_exports.string().max(80) }).optional()
17213
17336
  };
17214
17337
  var helpDataSchema = external_exports.object({
17215
17338
  usage: external_exports.string(),
17216
- commands: external_exports.array(external_exports.enum(["business", "capabilities", "credentials", "doctor", "eval", "harness", "login", "logout", "memory", "questions", "readiness", "research", "skill", "status", "strategies"]))
17339
+ commands: external_exports.array(external_exports.enum(["business", "capabilities", "credentials", "doctor", "eval", "harness", "login", "logout", "memory", "questions", "readiness", "research", "skill", "status", "strategies", "support"]))
17217
17340
  });
17218
17341
  var versionDataSchema = external_exports.object({ version: external_exports.string() });
17219
17342
  var skillDataSchema = external_exports.discriminatedUnion("action", [
@@ -17223,6 +17346,13 @@ var skillDataSchema = external_exports.discriminatedUnion("action", [
17223
17346
  var capabilitiesDataSchema = external_exports.object({
17224
17347
  expertise: external_exports.object({ manifestVersion: external_exports.string(), competencies: external_exports.array(external_exports.object({ id: external_exports.string(), reference: external_exports.string(), sha256: external_exports.string(), reviewedOn: external_exports.string(), sources: external_exports.array(external_exports.string()) })) }).optional(),
17225
17348
  version: external_exports.string(),
17349
+ measurementMarkets: external_exports.array(external_exports.object({ iso: external_exports.string().length(2), name: external_exports.string().min(1) })),
17350
+ catalog: external_exports.object({ version: external_exports.string(), currency: external_exports.literal("usd"), monthlyCents: external_exports.object({
17351
+ corePack: external_exports.number().int().positive(),
17352
+ claudePerPack: external_exports.number().int().positive(),
17353
+ perplexityPerPack: external_exports.number().int().positive()
17354
+ }) }),
17355
+ support: external_exports.object({ email: external_exports.literal(SUPPORT_EMAIL) }),
17226
17356
  realLogin: external_exports.literal("available"),
17227
17357
  paidExecution: external_exports.literal("server_authorized"),
17228
17358
  commands: external_exports.object({
@@ -17240,15 +17370,21 @@ var capabilitiesDataSchema = external_exports.object({
17240
17370
  memory: external_exports.object({ actions: external_exports.array(external_exports.string()), options: external_exports.array(external_exports.string()) }).optional(),
17241
17371
  status: external_exports.object({ options: external_exports.array(external_exports.string()) }),
17242
17372
  readiness: external_exports.object({ options: external_exports.array(external_exports.string()) }),
17243
- strategies: external_exports.object({ options: external_exports.array(external_exports.string()) })
17373
+ strategies: external_exports.object({ options: external_exports.array(external_exports.string()) }),
17374
+ support: external_exports.object({ tags: external_exports.array(external_exports.enum(SUPPORT_TAGS)), options: external_exports.array(external_exports.string()) })
17244
17375
  })
17245
17376
  });
17246
17377
  var localDoctorDataSchema = external_exports.object({
17247
17378
  node: external_exports.string(),
17248
17379
  cliVersion: external_exports.string(),
17380
+ latestVersion: external_exports.string().nullable(),
17381
+ // Set only when npm could not be reached: the last version it named.
17382
+ lastKnownVersion: external_exports.string().nullable(),
17383
+ updateCheck: external_exports.enum(["on", "off"]),
17249
17384
  origin: external_exports.string(),
17250
17385
  credentialStorage: external_exports.union([backendKindSchema, external_exports.literal("none")]),
17251
17386
  videoTools: external_exports.object({ ytDlp: external_exports.boolean(), ffmpeg: external_exports.boolean() }),
17387
+ server: external_exports.enum(["reachable", "degraded", "unreachable"]),
17252
17388
  diagnostic: external_exports.literal("completed")
17253
17389
  });
17254
17390
  var loginDataSchema = external_exports.object({
@@ -17545,6 +17681,7 @@ var successfulResultSchema = external_exports.union([
17545
17681
  external_exports.object({ ...envelope2, command: external_exports.literal("harness"), mode: external_exports.literal("product"), data: harnessCoverageDataSchema }),
17546
17682
  ...harnessEvidenceDataSchema.map((schema) => external_exports.object({ ...envelope2, command: external_exports.literal("harness"), mode: external_exports.literal("product"), data: schema })),
17547
17683
  external_exports.object({ ...envelope2, command: external_exports.literal("harness"), mode: external_exports.literal("product"), data: harnessEvidenceExportDataSchema }),
17684
+ external_exports.object({ ...envelope2, command: external_exports.literal("support"), mode: external_exports.literal("product"), data: supportDataSchema }),
17548
17685
  external_exports.object({ ...envelope2, command: external_exports.literal("memory"), mode: external_exports.literal("product"), data: memoryListDataSchema }),
17549
17686
  external_exports.object({ ...envelope2, command: external_exports.literal("memory"), mode: external_exports.literal("product"), data: memoryChangeDataSchema }),
17550
17687
  external_exports.object({
@@ -17595,7 +17732,8 @@ var failedResultSchema = external_exports.object({
17595
17732
  ...originMarker,
17596
17733
  error: errorSchema,
17597
17734
  authorization: pendingAuthorizationViewSchema.optional(),
17598
- account: confirmedAccountSchema.optional()
17735
+ account: confirmedAccountSchema.optional(),
17736
+ support: supportDataSchema.optional()
17599
17737
  });
17600
17738
  var cliResultSchema = external_exports.union([successfulResultSchema, failedResultSchema]);
17601
17739
 
@@ -17684,7 +17822,7 @@ function retryAfter(response) {
17684
17822
  return typeof value === "string" && /^[0-9]{1,5}$/.test(value) ? Number(value) : null;
17685
17823
  }
17686
17824
  async function send(context, request) {
17687
- const headers2 = { accept: "application/json" };
17825
+ const headers2 = { accept: "application/json", ...versionHeaders };
17688
17826
  if (request.body !== void 0) {
17689
17827
  headers2["content-type"] = "application/json";
17690
17828
  }
@@ -17894,12 +18032,7 @@ function interpret(response, schema) {
17894
18032
  return { ok: true, data: parsed.data.data };
17895
18033
  }
17896
18034
  const failure2 = ProductApiErrorResponseSchema.safeParse(body);
17897
- if (failure2.success) {
17898
- return {
17899
- ok: false,
17900
- failure: { code: failure2.data.error.code, httpStatus: response.status, retryAfterSeconds: retryAfter(response) }
17901
- };
17902
- }
18035
+ if (failure2.success) return apiFailure(failure2.data.error.code, response.status, retryAfter(response));
17903
18036
  if (response.status >= 500) {
17904
18037
  throw transportError("SERVICE_UNAVAILABLE", "The Obi API is unavailable.", 6, true);
17905
18038
  }
@@ -18060,6 +18193,9 @@ async function readReadiness(context, accessToken) {
18060
18193
  responseLimitBytes: BUSINESS_REVIEW_RESPONSE_LIMIT_BYTES
18061
18194
  }), ProductReadinessResponseSchema);
18062
18195
  }
18196
+ async function sendSupportMessage(context, accessToken, message) {
18197
+ return interpretReview(await send(context, { method: "POST", path: "/api/v1/support", bearer: accessToken, body: SupportMessageSchema.parse(message) }), SupportMessageResponseSchema);
18198
+ }
18063
18199
 
18064
18200
  // src/refresh.ts
18065
18201
  import { randomBytes as randomBytes2 } from "node:crypto";
@@ -19210,7 +19346,7 @@ async function submitHarnessObservation(context, path) {
19210
19346
 
19211
19347
  // src/commands.ts
19212
19348
  import { access } from "node:fs/promises";
19213
- import { fileURLToPath } from "node:url";
19349
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
19214
19350
 
19215
19351
  // src/onboarding-guide.ts
19216
19352
  var READING_POLL_MS = 6e4;
@@ -20275,6 +20411,108 @@ function memoryText(data) {
20275
20411
  `;
20276
20412
  }
20277
20413
 
20414
+ // src/support-commands.ts
20415
+ import { createHash as createHash2 } from "node:crypto";
20416
+ import { release as release2 } from "node:os";
20417
+
20418
+ // src/last-error.ts
20419
+ import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
20420
+ import { join as join4 } from "node:path";
20421
+ var fileIn = (stateDirectory) => join4(stateDirectory, "last-error.json");
20422
+ async function recordLastError(stateDirectory, error51) {
20423
+ try {
20424
+ const parsed = lastErrorSchema.safeParse({ ...error51, at: (/* @__PURE__ */ new Date()).toISOString() });
20425
+ if (!parsed.success) return;
20426
+ await ensureStateDirectory(stateDirectory);
20427
+ await writeFile3(fileIn(stateDirectory), JSON.stringify(parsed.data), { mode: 384 });
20428
+ } catch {
20429
+ }
20430
+ }
20431
+ async function readLastError(stateDirectory) {
20432
+ try {
20433
+ const parsed = lastErrorSchema.safeParse(JSON.parse(await readFile6(fileIn(stateDirectory), "utf8")));
20434
+ return parsed.success ? parsed.data : null;
20435
+ } catch {
20436
+ return null;
20437
+ }
20438
+ }
20439
+
20440
+ // src/support-commands.ts
20441
+ async function probeServer(origin, timeoutMs) {
20442
+ try {
20443
+ const response = await exchange({
20444
+ url: new URL("/api/health", origin.origin),
20445
+ method: "GET",
20446
+ headers: { accept: "application/json" },
20447
+ timeoutMs
20448
+ });
20449
+ return response.status === 200 ? "reachable" : "degraded";
20450
+ } catch {
20451
+ return "unreachable";
20452
+ }
20453
+ }
20454
+ async function sendSupport(context, input) {
20455
+ const lastError = await readLastError(context.stateDirectory);
20456
+ const diagnostic = { cliVersion, node: process.version, os: `${process.platform} ${release2()} ${process.arch}`, lastError };
20457
+ let account2 = null;
20458
+ let reason = null;
20459
+ try {
20460
+ const located = await locateCredential(context);
20461
+ if (located === null) {
20462
+ reason = "AUTH_REQUIRED";
20463
+ } else {
20464
+ account2 = located.credential.account.email;
20465
+ const requestId = createHash2("sha256").update(JSON.stringify([
20466
+ context.origin.origin,
20467
+ account2,
20468
+ input.message,
20469
+ input.tag,
20470
+ (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
20471
+ ])).digest("hex").slice(0, 32);
20472
+ const { result } = await useCredential(
20473
+ context,
20474
+ located,
20475
+ (token) => sendSupportMessage(context, token, { requestId, ...input, diagnostic })
20476
+ );
20477
+ if (!result.ok) reason = result.failure.httpStatus >= 500 ? "SERVICE_UNAVAILABLE" : result.failure.code;
20478
+ }
20479
+ } catch (error51) {
20480
+ reason = error51 instanceof CliError ? error51.code : "INTERNAL_ERROR";
20481
+ }
20482
+ const diagnosticText = [
20483
+ `Message: ${input.message}`,
20484
+ `Tag: ${input.tag ?? "general"}`,
20485
+ `Obi CLI ${diagnostic.cliVersion} \xB7 Node ${diagnostic.node} \xB7 ${diagnostic.os}`,
20486
+ `Last error: ${lastError === null ? "none" : `${lastError.code} (${lastError.command}) at ${lastError.at}`}`,
20487
+ `Account: ${account2 ?? "not signed in"}`
20488
+ ].join("\n");
20489
+ const data = {
20490
+ delivery: reason === null ? "sent" : "not_sent",
20491
+ reason,
20492
+ supportEmail: SUPPORT_EMAIL,
20493
+ mailto: `mailto:${SUPPORT_EMAIL}`,
20494
+ tag: input.tag,
20495
+ account: account2,
20496
+ origin: context.origin.origin,
20497
+ diagnosticText
20498
+ };
20499
+ if (reason === null) return data;
20500
+ throw new CliError({
20501
+ code: "SUPPORT_NOT_SENT",
20502
+ message: `The message was not sent to Obi support (${reason}).`,
20503
+ exitCode: reason === "AUTH_REQUIRED" ? 3 : 6,
20504
+ retryable: reason !== "AUTH_REQUIRED",
20505
+ mode: "product",
20506
+ command: "support",
20507
+ nextAction: `Write to ${SUPPORT_EMAIL} and paste the block below (support.diagnosticText with --json). Never add a token or a credential file.`,
20508
+ support: data
20509
+ });
20510
+ }
20511
+ function supportText(data) {
20512
+ return `Sent to Obi support (${data.supportEmail}). The reply goes to ${sanitizeHumanText(data.account ?? "the account email")}.
20513
+ `;
20514
+ }
20515
+
20278
20516
  // src/scan-output.ts
20279
20517
  function heading(data) {
20280
20518
  const scored = `${data.scoredCells}/${data.expectedCells} answers scored from question version ${data.candidateVersion ?? "none"}.`;
@@ -20399,6 +20637,7 @@ function failureText(result) {
20399
20637
  lines.push(`Approving account: ${sanitizeHumanText(result.account.email)} (${result.account.role}).`);
20400
20638
  }
20401
20639
  lines.push(sanitizeHumanText(result.error.nextAction));
20640
+ if (result.support !== void 0) lines.push("", ...result.support.diagnosticText.split("\n").map(sanitizeHumanText));
20402
20641
  return `${lines.join("\n")}
20403
20642
  `;
20404
20643
  }
@@ -20441,11 +20680,12 @@ function toHumanOutput(result) {
20441
20680
  return `Obi \xB7 AI Search Employee
20442
20681
 
20443
20682
  Get started
20444
- 1. obi skill install
20445
- 2. In a new Claude Code or Cursor chat, type /obi.
20683
+ 1. In Claude Code or Cursor, type /obi.
20446
20684
  In Codex CLI, type $obi or choose Obi from /skills.
20447
20685
  In the desktop app, select Obi from the skills picker.
20448
- 3. Give Obi a website or ask to continue saved work.
20686
+ In OpenCode, type @obi.
20687
+ 2. Give Obi a website or ask to continue saved work.
20688
+ Every obi command registers the Skill; obi skill install repairs it.
20449
20689
 
20450
20690
  Commands
20451
20691
  ${usage2}
@@ -20457,11 +20697,11 @@ ${usage2}
20457
20697
  }
20458
20698
  if (result.command === "skill") {
20459
20699
  return result.data.action === "path" ? `${sanitizeHumanText(result.data.path)}
20460
- ` : `Obi Skill registered locally. In a new Claude Code or Cursor chat, type /obi. In Codex CLI, type $obi or choose Obi from /skills. In the desktop app, select Obi from the skills picker.
20700
+ ` : `Obi Skill registered locally. In a new Claude Code or Cursor chat, type /obi. In Codex CLI, type $obi or choose Obi from /skills. In the desktop app, select Obi from the skills picker. In OpenCode, type @obi.
20461
20701
  ${result.data.removed.length > 0 ? "Earlier Skill links of this package under another name were removed; use /obi.\n" : ""}`;
20462
20702
  }
20463
20703
  if (result.command === "capabilities") {
20464
- return "Available: business, capabilities, credentials, doctor, eval, harness, login, logout, memory, questions, readiness, research, skill, status, strategies. Scan execution requires server authorization and a separate customer decision.\n";
20704
+ return "Available: business, capabilities, credentials, doctor, eval, harness, login, logout, memory, questions, readiness, research, skill, status, strategies, support. Scan execution requires server authorization and a separate customer decision.\n";
20465
20705
  }
20466
20706
  if (result.command === "eval") {
20467
20707
  if ("evaluation" in result.data) {
@@ -20476,6 +20716,7 @@ Next: ${sanitizeHumanText(result.data.nextStep)}
20476
20716
  return result.data.action === "list" ? connectionListText(result.data) : revocationText(result.data);
20477
20717
  }
20478
20718
  if (result.command === "memory") return memoryText(result.data);
20719
+ if (result.command === "support") return supportText(result.data);
20479
20720
  if (result.command === "doctor") {
20480
20721
  if (result.mode === "test") {
20481
20722
  return `Synthetic test protocol reachable. State: ${String(result.data.state)}.
@@ -20483,7 +20724,14 @@ Next: ${sanitizeHumanText(result.data.nextStep)}
20483
20724
  }
20484
20725
  const storage = result.data.credentialStorage === "none" ? "no stored connection" : `connection stored in ${storageLabels[result.data.credentialStorage]}`;
20485
20726
  const video = result.data.videoTools.ytDlp ? `video reading available${result.data.videoTools.ffmpeg ? " with frames" : " (captions only; frames need ffmpeg)"}` : "video reading needs yt-dlp";
20486
- return `Local diagnostic completed for ${sanitizeHumanText(result.data.origin)}: ${storage}; ${video}. No request was sent.
20727
+ const server = {
20728
+ reachable: "Obi server reachable.",
20729
+ degraded: `Obi server degraded: try again later, and write to ${SUPPORT_EMAIL} if it lasts.`,
20730
+ unreachable: `Obi server unreachable: check the network connection, and write to ${SUPPORT_EMAIL} if it lasts.`
20731
+ }[result.data.server];
20732
+ const { cliVersion: installed, latestVersion: latest } = result.data;
20733
+ const version3 = result.data.updateCheck === "off" ? `CLI ${sanitizeHumanText(installed)} (update check off)` : latest === null ? `CLI ${sanitizeHumanText(installed)} (latest version unknown: npm could not be reached${result.data.lastKnownVersion === null ? "" : `, last known ${sanitizeHumanText(result.data.lastKnownVersion)}`})` : isOlderRelease(installed, latest) ? `CLI ${sanitizeHumanText(installed)}, latest ${sanitizeHumanText(latest)}: update with ${CLI_UPDATE_COMMAND}` : `CLI ${sanitizeHumanText(installed)}, the latest`;
20734
+ return `Local diagnostic completed for ${sanitizeHumanText(result.data.origin)}: ${version3}; ${storage}; ${video}. ${server}
20487
20735
  `;
20488
20736
  }
20489
20737
  if (result.command === "login") {
@@ -20819,9 +21067,6 @@ async function confirmAccount(account2) {
20819
21067
  }
20820
21068
  }
20821
21069
 
20822
- // src/version.ts
20823
- var cliVersion = "0.1.8";
20824
-
20825
21070
  // src/login.ts
20826
21071
  var loginRequestTimeoutMs = 1e4;
20827
21072
  var maxLinkLength = 200;
@@ -21031,7 +21276,7 @@ function accountMismatch(account2, revoked, expected) {
21031
21276
  }
21032
21277
  async function confirmAndStore(context, store, pair, receivedAt, expectedEmail) {
21033
21278
  const api = context;
21034
- const release2 = guardNewFamily(api, pair);
21279
+ const release4 = guardNewFamily(api, pair);
21035
21280
  let stored = false;
21036
21281
  try {
21037
21282
  const status = await readAccountStatus(api, pair.accessToken);
@@ -21062,7 +21307,7 @@ async function confirmAndStore(context, store, pair, receivedAt, expectedEmail)
21062
21307
  }
21063
21308
  await store.writeCredential(credentialFromPair(context.origin.origin, pair, confirmed, receivedAt));
21064
21309
  stored = true;
21065
- release2();
21310
+ release4();
21066
21311
  });
21067
21312
  try {
21068
21313
  await store.removePending();
@@ -21082,7 +21327,7 @@ async function confirmAndStore(context, store, pair, receivedAt, expectedEmail)
21082
21327
  }
21083
21328
  throw error51;
21084
21329
  } finally {
21085
- release2();
21330
+ release4();
21086
21331
  }
21087
21332
  }
21088
21333
  async function login(session, options) {
@@ -21221,12 +21466,12 @@ async function proceedToFirstScan(context, candidateVersion) {
21221
21466
  }
21222
21467
 
21223
21468
  // src/research-commands.ts
21224
- import { readFile as readFile6, stat as stat6 } from "node:fs/promises";
21469
+ import { readFile as readFile7, stat as stat6 } from "node:fs/promises";
21225
21470
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
21226
21471
  import { resolve as resolve2 } from "node:path";
21227
21472
 
21228
21473
  // ../geo-engine/src/geo/host-acquisition.ts
21229
- import { createHash as createHash5 } from "node:crypto";
21474
+ import { createHash as createHash6 } from "node:crypto";
21230
21475
 
21231
21476
  // ../geo-engine/src/url-safety.ts
21232
21477
  var PRIVATE_IP_RE = /^(127\.|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.|0\.)/;
@@ -21397,7 +21642,7 @@ async function safeFetch(url3, opts = {}) {
21397
21642
  if (!REDIRECT_STATUSES.has(res.status)) return res;
21398
21643
  const location = res.headers.get("location");
21399
21644
  if (!location) return res;
21400
- await release(res);
21645
+ await release3(res);
21401
21646
  let next;
21402
21647
  try {
21403
21648
  next = new URL(location, current).href;
@@ -21408,7 +21653,7 @@ async function safeFetch(url3, opts = {}) {
21408
21653
  }
21409
21654
  throw new TooManyRedirectsError(url3, maxRedirects);
21410
21655
  }
21411
- async function release(res) {
21656
+ async function release3(res) {
21412
21657
  await res.body?.cancel().catch((err) => {
21413
21658
  console.warn("[safe-fetch] cancelling an unread body failed:", err);
21414
21659
  });
@@ -21416,7 +21661,7 @@ async function release(res) {
21416
21661
  async function readCapped(res, maxBytes = MAX_RESPONSE_BYTES) {
21417
21662
  const declared = Number(res.headers.get("content-length"));
21418
21663
  if (Number.isFinite(declared) && declared > maxBytes) {
21419
- await release(res);
21664
+ await release3(res);
21420
21665
  return "";
21421
21666
  }
21422
21667
  if (!res.body) return (await res.text()).slice(0, maxBytes);
@@ -21582,7 +21827,7 @@ function createVettedTransport(options = {}) {
21582
21827
  }
21583
21828
 
21584
21829
  // ../geo-engine/src/geo/page-evidence.ts
21585
- import { createHash as createHash2 } from "node:crypto";
21830
+ import { createHash as createHash3 } from "node:crypto";
21586
21831
 
21587
21832
  // ../node_modules/parse5/dist/common/unicode.js
21588
21833
  var UNDEFINED_CODE_POINTS = /* @__PURE__ */ new Set([
@@ -29502,7 +29747,7 @@ function* descendants(node) {
29502
29747
  }
29503
29748
  }
29504
29749
  function sha256(s) {
29505
- return createHash2("sha256").update(s).digest("hex");
29750
+ return createHash3("sha256").update(s).digest("hex");
29506
29751
  }
29507
29752
  var SKIPPED = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "svg", "iframe", "object", "canvas", "select", "head"]);
29508
29753
  var HEADING = /^h([1-6])$/;
@@ -29793,7 +30038,7 @@ function headingTexts(html, level) {
29793
30038
  }
29794
30039
 
29795
30040
  // ../geo-engine/src/geo/selected-page-evidence.ts
29796
- import { createHash as createHash3 } from "node:crypto";
30041
+ import { createHash as createHash4 } from "node:crypto";
29797
30042
 
29798
30043
  // ../geo-engine/src/geo/material-fact-contract.ts
29799
30044
  var text = external_exports.string().trim().min(1);
@@ -29899,10 +30144,10 @@ var SelectedPageEvidenceSchema = external_exports.strictObject({
29899
30144
  function selectedPageId(pageUrl) {
29900
30145
  const selected = new URL(pageUrl);
29901
30146
  selected.hash = "";
29902
- return `page:${createHash3("sha256").update(selected.href).digest("hex")}`;
30147
+ return `page:${createHash4("sha256").update(selected.href).digest("hex")}`;
29903
30148
  }
29904
30149
  function observationBodyHash(body) {
29905
- return createHash3("sha256").update(body).digest("hex");
30150
+ return createHash4("sha256").update(body).digest("hex");
29906
30151
  }
29907
30152
  function sameSelectedHost(selectedUrl, finalUrl) {
29908
30153
  const host = (value) => new URL(value).hostname.toLowerCase().replace(/^www\./, "");
@@ -30354,14 +30599,14 @@ function evaluateAccess(parsed, token, url3, fallbackTokens = []) {
30354
30599
  }
30355
30600
 
30356
30601
  // ../geo-engine/src/geo/readiness/findings.ts
30357
- import { createHash as createHash4 } from "node:crypto";
30602
+ import { createHash as createHash5 } from "node:crypto";
30358
30603
  function issueId(origin, checkId, scopeKey) {
30359
- return createHash4("sha256").update(`${origin.toLowerCase()}
30604
+ return createHash5("sha256").update(`${origin.toLowerCase()}
30360
30605
  ${checkId}
30361
30606
  ${scopeKey}`).digest("hex").slice(0, 16);
30362
30607
  }
30363
30608
  function fingerprint(body) {
30364
- return body === null ? null : createHash4("sha256").update(body).digest("hex");
30609
+ return body === null ? null : createHash5("sha256").update(body).digest("hex");
30365
30610
  }
30366
30611
  function makeFinding({ origin, scopeKey, policyChoice = false, materiality = "site", ...rest }) {
30367
30612
  return { id: issueId(origin, rest.checkId, scopeKey), policyChoice, materiality, ...rest };
@@ -30864,7 +31109,7 @@ function validateState(raw) {
30864
31109
  return state;
30865
31110
  }
30866
31111
  function matchesFingerprint(observation) {
30867
- return observation.body === null ? observation.bodyHash === null : observation.bodyHash === createHash5("sha256").update(observation.body).digest("hex");
31112
+ return observation.body === null ? observation.bodyHash === null : observation.bodyHash === createHash6("sha256").update(observation.body).digest("hex");
30868
31113
  }
30869
31114
  function pendingProbeRequests(state) {
30870
31115
  const home = state.resources.find((resource) => resource.kind === "page" && resource.url === `${state.origin}/`);
@@ -32635,7 +32880,7 @@ async function readJson(path, maxBytes = maxCorpusBytes) {
32635
32880
  try {
32636
32881
  const info = await stat6(path);
32637
32882
  if (!info.isFile() || info.size > maxBytes) throw fileError(`The research file is not a regular JSON file within the ${maxBytes / 1024 / 1024} MiB local limit.`);
32638
- const content = await readFile6(path);
32883
+ const content = await readFile7(path);
32639
32884
  if (content.byteLength > maxBytes) throw fileError(`The research file exceeds the ${maxBytes / 1024 / 1024} MiB local limit.`);
32640
32885
  return JSON.parse(content.toString("utf8"));
32641
32886
  } catch (error51) {
@@ -32823,8 +33068,8 @@ async function captureResearch(input, checkpoint, output) {
32823
33068
 
32824
33069
  // src/research-video.ts
32825
33070
  import { spawn as spawn2 } from "node:child_process";
32826
- import { mkdir as mkdir2, readdir as readdir3, readFile as readFile7, rm as rm2 } from "node:fs/promises";
32827
- import { join as join4, resolve as resolve3 } from "node:path";
33071
+ import { mkdir as mkdir2, readdir as readdir3, readFile as readFile8, rm as rm2 } from "node:fs/promises";
33072
+ import { join as join5, resolve as resolve3 } from "node:path";
32828
33073
  var maxToolOutputBytes = 256 * 1024;
32829
33074
  var runTool = (executable, args, timeoutMs) => new Promise((done, fail) => {
32830
33075
  const child = spawn2(executable, [...args], { stdio: ["ignore", "pipe", "pipe"], shell: false });
@@ -32923,12 +33168,12 @@ async function readVideo(rawUrl, outputDir, frames, runner = runTool) {
32923
33168
  const directory = resolve3(outputDir);
32924
33169
  await mkdir2(directory, { recursive: true });
32925
33170
  const base = ["--ignore-config", "--no-playlist", "--quiet", "--no-warnings"];
32926
- const target = ["-o", join4(templateSafe(directory), "video.%(ext)s")];
33171
+ const target = ["-o", join5(templateSafe(directory), "video.%(ext)s")];
32927
33172
  const failed = (message, nextAction) => videoError("RESEARCH_VIDEO_FAILED", message, nextAction);
32928
33173
  const metadata = await runner("yt-dlp", [...base, "--skip-download", "--write-info-json", ...target, url3.href], 18e4).catch((error51) => {
32929
33174
  throw error51.code === "ENOENT" ? missingTool(frames) : failed("yt-dlp did not finish reading this video page.", "Retry once; a slow or blocked network leaves the video unread.");
32930
33175
  });
32931
- const info = metadata.exitCode === 0 ? JSON.parse(await readFile7(join4(directory, "video.info.json"), "utf8")) : null;
33176
+ const info = metadata.exitCode === 0 ? JSON.parse(await readFile8(join5(directory, "video.info.json"), "utf8")) : null;
32932
33177
  if (!info) {
32933
33178
  throw failed("yt-dlp could not read this video page.", "Check the address in a browser; a private or region-locked video stays unread.");
32934
33179
  }
@@ -32950,11 +33195,11 @@ async function readVideo(rawUrl, outputDir, frames, runner = runTool) {
32950
33195
  if (subtitles?.exitCode !== 0 || !file2) {
32951
33196
  throw failed("The video has captions but their download failed.", "Retry in a minute; the platform may be rate-limiting.");
32952
33197
  }
32953
- segments = parseVtt(await readFile7(join4(directory, file2), "utf8"));
33198
+ segments = parseVtt(await readFile8(join5(directory, file2), "utf8"));
32954
33199
  }
32955
33200
  const duration3 = typeof info.duration === "number" ? info.duration : null;
32956
33201
  const frameFiles = [];
32957
- const output = join4(directory, "transcript.json");
33202
+ const output = join5(directory, "transcript.json");
32958
33203
  const save = () => writeJsonAtomic(
32959
33204
  output,
32960
33205
  {
@@ -32974,7 +33219,7 @@ async function readVideo(rawUrl, outputDir, frames, runner = runTool) {
32974
33219
  throw failed("The video's length is unknown, so frames cannot be spaced.", "Retry with --frames 0 to keep the captions only.");
32975
33220
  }
32976
33221
  if (frames > 0 && duration3) {
32977
- const removeMedia = async () => Promise.all((await readdir3(directory)).filter((file2) => file2.startsWith("media.")).map((file2) => rm2(join4(directory, file2), { force: true })));
33222
+ const removeMedia = async () => Promise.all((await readdir3(directory)).filter((file2) => file2.startsWith("media.")).map((file2) => rm2(join5(directory, file2), { force: true })));
32978
33223
  await removeMedia();
32979
33224
  try {
32980
33225
  const download = await runner("yt-dlp", [
@@ -32982,7 +33227,7 @@ async function readVideo(rawUrl, outputDir, frames, runner = runTool) {
32982
33227
  "-f",
32983
33228
  "b[height<=480]/bv*[height<=480]/w",
32984
33229
  "-o",
32985
- join4(templateSafe(directory), "media.%(ext)s"),
33230
+ join5(templateSafe(directory), "media.%(ext)s"),
32986
33231
  url3.href
32987
33232
  ], 6e5).catch(() => {
32988
33233
  throw failed("The video download did not finish.", "Retry with --frames 0 to keep the captions only.");
@@ -33002,14 +33247,14 @@ async function readVideo(rawUrl, outputDir, frames, runner = runTool) {
33002
33247
  "-ss",
33003
33248
  String(atSeconds),
33004
33249
  "-i",
33005
- join4(directory, media),
33250
+ join5(directory, media),
33006
33251
  "-frames:v",
33007
33252
  "1",
33008
33253
  "-vf",
33009
33254
  "scale=1024:-2",
33010
- join4(directory, file2)
33255
+ join5(directory, file2)
33011
33256
  ], 6e4).catch(() => null);
33012
- if (extracted?.exitCode === 0) frameFiles.push({ file: join4(directory, file2), atSeconds });
33257
+ if (extracted?.exitCode === 0) frameFiles.push({ file: join5(directory, file2), atSeconds });
33013
33258
  }
33014
33259
  } finally {
33015
33260
  await removeMedia();
@@ -33128,13 +33373,15 @@ async function showAlphaStrategies(context) {
33128
33373
  // src/skill-install.ts
33129
33374
  import { lstat as lstat3, mkdir as mkdir3, readlink, realpath, symlink, unlink as unlink5 } from "node:fs/promises";
33130
33375
  import { homedir as homedir2 } from "node:os";
33131
- import { join as join5, sep } from "node:path";
33376
+ import { join as join6, sep } from "node:path";
33377
+ import { fileURLToPath } from "node:url";
33132
33378
  var skillDirectories = [".agents/skills", ".claude/skills"];
33133
33379
  var alphaSkillName = "heysurface";
33380
+ var messageLimit = 300;
33134
33381
  function installError(message) {
33135
33382
  return new CliError({
33136
33383
  code: "SKILL_INSTALL_FAILED",
33137
- message,
33384
+ message: message.length > messageLimit ? `${message.slice(0, messageLimit - 1)}\u2026` : message,
33138
33385
  exitCode: 4,
33139
33386
  retryable: false,
33140
33387
  nextAction: "Check the personal Skill directories and run obi skill install again. Existing files are never replaced.",
@@ -33164,7 +33411,7 @@ async function linksIntoPackage(path, packageRoot) {
33164
33411
  return target === packageRoot || target.startsWith(`${packageRoot}${sep}`);
33165
33412
  } catch {
33166
33413
  try {
33167
- return await readlink(path) === join5(packageRoot, "skills", alphaSkillName);
33414
+ return await readlink(path) === join6(packageRoot, "skills", alphaSkillName);
33168
33415
  } catch {
33169
33416
  return false;
33170
33417
  }
@@ -33173,37 +33420,70 @@ async function linksIntoPackage(path, packageRoot) {
33173
33420
  async function installSkill(sourceDirectory) {
33174
33421
  const source = await realpath(sourceDirectory);
33175
33422
  const home = homedir2();
33176
- const destinations = skillDirectories.map((directory) => join5(home, directory, "obi"));
33423
+ const destinations = skillDirectories.map((directory) => join6(home, directory, "obi"));
33424
+ const registered = async (destination) => {
33425
+ try {
33426
+ return await realpath(destination) === source;
33427
+ } catch {
33428
+ return false;
33429
+ }
33430
+ };
33177
33431
  for (const destination of destinations) {
33178
- if (!await existingPath(destination)) continue;
33432
+ if (!await existingPath(destination) || await registered(destination)) continue;
33433
+ let target = null;
33179
33434
  try {
33180
- if (await realpath(destination) === source) continue;
33435
+ if ((await lstat3(destination)).isSymbolicLink()) target = await readlink(destination);
33181
33436
  } catch {
33437
+ continue;
33182
33438
  }
33183
- throw installError(`A Skill entry already exists at ${destination}.`);
33439
+ throw installError(target === null ? `A Skill entry already exists: a file or folder this package did not create. Move it, then run obi skill install. It is at ${destination}` : `A Skill entry already exists: a link outside this installation. To use this installation, remove that link, then run obi skill install. The link: ${destination} -> ${target}`);
33184
33440
  }
33185
33441
  for (const destination of destinations) {
33186
- if (await existingPath(destination)) continue;
33442
+ if (await registered(destination)) continue;
33187
33443
  try {
33188
- await mkdir3(join5(destination, ".."), { recursive: true });
33444
+ await mkdir3(join6(destination, ".."), { recursive: true });
33189
33445
  await symlink(source, destination, process.platform === "win32" ? "junction" : "dir");
33190
33446
  } catch {
33191
- throw installError(`Could not register the Obi Skill at ${destination}.`);
33447
+ if (await registered(destination)) continue;
33448
+ throw installError(`Could not register the Obi Skill. Check that its folder can be written, then run obi skill install. The link to make: ${destination}`);
33192
33449
  }
33193
33450
  }
33194
33451
  const removed = [];
33195
- const packageRoot = await realpath(join5(source, "..", ".."));
33196
- for (const alpha of skillDirectories.map((directory) => join5(home, directory, alphaSkillName))) {
33452
+ const packageRoot = await realpath(join6(source, "..", ".."));
33453
+ for (const alpha of skillDirectories.map((directory) => join6(home, directory, alphaSkillName))) {
33197
33454
  if (!await linksIntoPackage(alpha, packageRoot)) continue;
33198
33455
  try {
33199
33456
  await unlink5(alpha);
33200
- } catch {
33457
+ } catch (error51) {
33458
+ if (error51.code === "ENOENT") continue;
33201
33459
  throw installError(`Could not retire the earlier Skill link at ${alpha}.`);
33202
33460
  }
33203
33461
  removed.push(alpha);
33204
33462
  }
33205
33463
  return { paths: destinations, removed };
33206
33464
  }
33465
+ function packagedSkillDirectory() {
33466
+ return fileURLToPath(new URL("../skills/obi", import.meta.url));
33467
+ }
33468
+ async function registerSkillLinks(sourceDirectory, env) {
33469
+ if (env.OBI_SKILL_LINKS === "off") return { state: "off" };
33470
+ try {
33471
+ const source = await realpath(sourceDirectory);
33472
+ let present3 = true;
33473
+ for (const directory of skillDirectories) {
33474
+ try {
33475
+ if (await realpath(join6(homedir2(), directory, "obi")) !== source) present3 = false;
33476
+ } catch {
33477
+ present3 = false;
33478
+ }
33479
+ }
33480
+ if (present3) return { state: "present" };
33481
+ await installSkill(sourceDirectory);
33482
+ return { state: "created" };
33483
+ } catch (error51) {
33484
+ return { state: "failed", reason: error51 instanceof CliError ? error51.message : "The Obi Skill could not be registered." };
33485
+ }
33486
+ }
33207
33487
 
33208
33488
  // src/transport.ts
33209
33489
  var loopbackOrigin = /^http:\/\/(127\.0\.0\.1|\[::1\]):([1-9][0-9]{0,4})$/;
@@ -33328,7 +33608,7 @@ async function fetchTestStatus(origin, timeoutMs) {
33328
33608
  }
33329
33609
 
33330
33610
  // src/commands.ts
33331
- var usage = "obi <business|capabilities|credentials|doctor|eval|harness|login|logout|memory|questions|readiness|research|skill|status|strategies> [--json]\n obi eval <start <domain>|list> [--json]\n Evaluation commands: add --eval-case <id> to status, business, harness, readiness, questions and strategies\n obi harness coverage [--json]\n obi harness activate --free [--json]\n obi harness show --stage <business|questions|strategies> [--packs <1-40> for questions review] [--json]\n obi harness submit --stage <business|questions|strategies> --input <file> [--json]\n obi harness outcomes --task-id <UUID> [--cursor <event-id>] [--limit <1-50>] [--json]\n obi harness observe --input <file> [--json]\n obi harness purchase <quote|prepare|status|resume|change> [--input <quote-and-basket.json>; resume: quote ID only] [--confirm-checkout for prepare] [--confirm-change for change] [--json]\n obi harness artifact <save|show|history|export> --work-id <UUID> [--input <JSON file>] [--revision <n>] [--output <new file>] [--cursor <opaque>] [--limit <1-100>] [--json]\n obi harness evidence --work-id <UUID> [--kind claims|sections|measurements] [--cursor <opaque>] [--query <text>] [--url <exact URL>] [--all --output <file>] [--json]\n obi research crawl <domain> --checkpoint <file> --output <file> [--batch-size <1-100>] [--locale <tag>] [--pages priority|all] [--max-pages <n|none>] [--retry-failed|--refresh] [--json]\n obi research capture --input <selected-page-file> --checkpoint <file> --output <corpus-file> [--json]\n obi research readiness --input <corpus-file> --output <report-file> [--json]\n obi research video <url> --output <directory> [--frames <0-30>] [--json]\n obi memory <list|add --kind <preference|constraint|decision> --text <words>|remove <id>> [--eval-case <id>] [--json]\n obi questions show [--json]\n obi questions prepare --confirm-preparation [--json]\n obi questions edit <id> --candidate-version <n> --text <wording> [--json]\n obi questions proceed --candidate-version <n> --confirm-scan [--json]\n obi questions results [--run latest|first|<run ID>] [--from <question position>] [--json]\n obi readiness [--json]\n obi strategies [--json]\n obi skill <path|install> [--json]\n obi credentials <list|revoke <credentialId>> [--force]\n obi business start --confirm-domain <domain>\n obi business show\n obi business discover --snapshot <token> --confirm-discovery\n obi business clarify-identity --snapshot <token> --company <name>\n obi business clarify-fact <itemId> --snapshot <token> --field-path <path> --text <answer> --company <current name> --company-wide [--segment-name <name> for segments.append]\n obi business challenge <itemId> --snapshot <token> --reason <reason>\n obi business correct <itemId> --snapshot <token> --claim-id <id> --field-path <path> --text <affirmative correction> --company <current name> --company-wide\n obi business remove <competitorItemId> --snapshot <token> [--reason <reason>]\n obi business <add-competitor|remove-competitor|readd-competitor> <domain> --snapshot <token> [--reason <reason>]\n obi business confirm --snapshot <token> --confirm-review\n Global options: --json, --timeout-ms <100-30000>; login: --expect-email <email> [--storage file]; status/doctor test: --test-server <origin>";
33611
+ var usage = 'obi <business|capabilities|credentials|doctor|eval|harness|login|logout|memory|questions|readiness|research|skill|status|strategies|support> [--json]\n obi support "<message>" [--tag market-request|scope-request] [--json]\n obi eval <start <domain>|list> [--json]\n Evaluation commands: add --eval-case <id> to status, business, harness, readiness, questions and strategies\n obi harness coverage [--json]\n obi harness activate --free [--json]\n obi harness show --stage <business|questions|strategies> [--packs <1-40> for questions review] [--json]\n obi harness submit --stage <business|questions|strategies> --input <file> [--json]\n obi harness outcomes --task-id <UUID> [--cursor <event-id>] [--limit <1-50>] [--json]\n obi harness observe --input <file> [--json]\n obi harness purchase <quote|prepare|status|resume|change> [--input <quote-and-basket.json>; resume: quote ID only] [--confirm-checkout for prepare] [--confirm-change for change] [--json]\n obi harness artifact <save|show|history|export> --work-id <UUID> [--input <JSON file>] [--revision <n>] [--output <new file>] [--cursor <opaque>] [--limit <1-100>] [--json]\n obi harness evidence --work-id <UUID> [--kind claims|sections|measurements] [--cursor <opaque>] [--query <text>] [--url <exact URL>] [--all --output <file>] [--json]\n obi research crawl <domain> --checkpoint <file> --output <file> [--batch-size <1-100>] [--locale <tag>] [--pages priority|all] [--max-pages <n|none>] [--retry-failed|--refresh] [--json]\n obi research capture --input <selected-page-file> --checkpoint <file> --output <corpus-file> [--json]\n obi research readiness --input <corpus-file> --output <report-file> [--json]\n obi research video <url> --output <directory> [--frames <0-30>] [--json]\n obi memory <list|add --kind <preference|constraint|decision> --text <words>|remove <id>> [--eval-case <id>] [--json]\n obi questions show [--json]\n obi questions prepare --confirm-preparation [--json]\n obi questions edit <id> --candidate-version <n> --text <wording> [--json]\n obi questions proceed --candidate-version <n> --confirm-scan [--json]\n obi questions results [--run latest|first|<run ID>] [--from <question position>] [--json]\n obi readiness [--json]\n obi strategies [--json]\n obi skill <path|install> [--json]\n obi credentials <list|revoke <credentialId>> [--force]\n obi business start --confirm-domain <domain>\n obi business show\n obi business discover --snapshot <token> --confirm-discovery\n obi business clarify-identity --snapshot <token> --company <name>\n obi business clarify-fact <itemId> --snapshot <token> --field-path <path> --text <answer> --company <current name> --company-wide [--segment-name <name> for segments.append]\n obi business challenge <itemId> --snapshot <token> --reason <reason>\n obi business correct <itemId> --snapshot <token> --claim-id <id> --field-path <path> --text <affirmative correction> --company <current name> --company-wide\n obi business remove <competitorItemId> --snapshot <token> [--reason <reason>]\n obi business <add-competitor|remove-competitor|readd-competitor> <domain> --snapshot <token> [--reason <reason>]\n obi business confirm --snapshot <token> --confirm-review\n Global options: --json, --timeout-ms <100-30000>; login: --expect-email <email> [--storage file]; status/doctor test: --test-server <origin>';
33332
33612
  function success2(command, mode, data) {
33333
33613
  return cliResultSchema.parse({ schemaVersion: 1, command, mode, ok: true, data });
33334
33614
  }
@@ -33355,14 +33635,19 @@ function statusResult(status) {
33355
33635
  }
33356
33636
  return success2("status", "test", status);
33357
33637
  }
33358
- async function localDoctor(runtime) {
33638
+ async function localDoctor(runtime, timeoutMs) {
33359
33639
  const located = await locateCredential(runtime);
33640
+ const updates = await runtime.updates;
33360
33641
  return success2("doctor", "local", {
33361
33642
  node: process.version,
33362
33643
  cliVersion,
33644
+ latestVersion: updates?.state === "on" && updates.reached ? updates.latest : null,
33645
+ lastKnownVersion: updates?.state === "on" && !updates.reached ? updates.latest : null,
33646
+ updateCheck: updates?.state ?? "off",
33363
33647
  origin: runtime.origin.origin,
33364
33648
  credentialStorage: located === null ? "none" : located.store.backend.kind,
33365
33649
  videoTools: await videoTools(),
33650
+ server: await probeServer(runtime.origin, timeoutMs),
33366
33651
  diagnostic: "completed"
33367
33652
  });
33368
33653
  }
@@ -33375,7 +33660,7 @@ async function executeCommand(args, runtime) {
33375
33660
  if (args.command === "help") {
33376
33661
  return success2("help", "local", {
33377
33662
  usage,
33378
- commands: ["business", "capabilities", "credentials", "doctor", "eval", "harness", "login", "logout", "memory", "questions", "readiness", "research", "skill", "status", "strategies"]
33663
+ commands: ["business", "capabilities", "credentials", "doctor", "eval", "harness", "login", "logout", "memory", "questions", "readiness", "research", "skill", "status", "strategies", "support"]
33379
33664
  });
33380
33665
  }
33381
33666
  if (args.command === "version") {
@@ -33384,6 +33669,13 @@ async function executeCommand(args, runtime) {
33384
33669
  if (args.command === "capabilities") {
33385
33670
  return success2("capabilities", "local", {
33386
33671
  version: cliVersion,
33672
+ measurementMarkets: SUPPORTED_MARKETS,
33673
+ catalog: { version: CLI_DOLLAR_CATALOG_VERSION, currency: "usd", monthlyCents: {
33674
+ corePack: CLI_DOLLAR_PRICES.core,
33675
+ claudePerPack: CLI_DOLLAR_PRICES.claude,
33676
+ perplexityPerPack: CLI_DOLLAR_PRICES.perplexity
33677
+ } },
33678
+ support: { email: SUPPORT_EMAIL },
33387
33679
  realLogin: "available",
33388
33680
  paidExecution: "server_authorized",
33389
33681
  expertise: { manifestVersion: AI_SEARCH_COMPETENCY_VERSION, competencies: AI_SEARCH_COMPETENCIES },
@@ -33402,13 +33694,14 @@ async function executeCommand(args, runtime) {
33402
33694
  skill: { actions: ["path", "install"], options: ["--json"] },
33403
33695
  status: { options: ["--json", "--eval-case", "--test-server", "--timeout-ms"] },
33404
33696
  readiness: { options: ["--json", "--eval-case", "--timeout-ms"] },
33405
- strategies: { options: ["--json", "--eval-case", "--timeout-ms"] }
33697
+ strategies: { options: ["--json", "--eval-case", "--timeout-ms"] },
33698
+ support: { tags: [...SUPPORT_TAGS], options: ["--json", "--tag", "--timeout-ms"] }
33406
33699
  }
33407
33700
  });
33408
33701
  }
33409
33702
  if (args.command === "doctor") {
33410
33703
  if (args.testServer === void 0) {
33411
- return localDoctor(runtime);
33704
+ return localDoctor(runtime, args.timeoutMs);
33412
33705
  }
33413
33706
  const status = await fetchTestStatus(args.testServer, args.timeoutMs);
33414
33707
  return success2("doctor", "test", {
@@ -33418,7 +33711,7 @@ async function executeCommand(args, runtime) {
33418
33711
  });
33419
33712
  }
33420
33713
  if (args.command === "skill") {
33421
- const path = fileURLToPath(new URL("../skills/obi/SKILL.md", import.meta.url));
33714
+ const path = fileURLToPath2(new URL("../skills/obi/SKILL.md", import.meta.url));
33422
33715
  try {
33423
33716
  await access(path);
33424
33717
  } catch {
@@ -33431,7 +33724,7 @@ async function executeCommand(args, runtime) {
33431
33724
  mode: "local"
33432
33725
  });
33433
33726
  }
33434
- return success2("skill", "local", args.skillAction === "install" ? { action: "install", ...await installSkill(fileURLToPath(new URL("../skills/obi", import.meta.url))) } : { action: "path", path });
33727
+ return success2("skill", "local", args.skillAction === "install" ? { action: "install", ...await installSkill(fileURLToPath2(new URL("../skills/obi", import.meta.url))) } : { action: "path", path });
33435
33728
  }
33436
33729
  if (args.command === "login") {
33437
33730
  return success2("login", "product", await login(session, {
@@ -33488,6 +33781,9 @@ async function executeCommand(args, runtime) {
33488
33781
  }
33489
33782
  ) : args.researchAction === "video" ? await readVideo(args.researchVideoUrl, args.researchOutput, args.researchFrames ?? 0) : args.researchAction === "capture" ? await captureResearch(args.researchInput, args.researchCheckpoint, args.researchOutput) : await assessResearch(args.researchInput, args.researchOutput));
33490
33783
  }
33784
+ if (args.command === "support") {
33785
+ return success2("support", "product", await sendSupport(session, { message: args.supportMessage, tag: args.supportTag ?? null }));
33786
+ }
33491
33787
  if (args.command === "memory") {
33492
33788
  if (args.memoryAction === "list") return success2("memory", "product", await listMemory(session));
33493
33789
  return success2("memory", "product", await changeMemory(session, args.memoryAction === "add" ? { op: "add", kind: args.memoryKind, text: args.memoryText } : { op: "remove", id: args.memoryEntryId }));
@@ -33540,23 +33836,126 @@ async function executeCommand(args, runtime) {
33540
33836
  return success2("status", "product", await readStatus(session));
33541
33837
  }
33542
33838
 
33839
+ // src/start.ts
33840
+ import { join as join7 } from "node:path";
33841
+ var linkStates = {
33842
+ created: "created",
33843
+ present: "present",
33844
+ off: "off (OBI_SKILL_LINKS), run obi skill install"
33845
+ };
33846
+ function renderStart(input) {
33847
+ const links = input.registration.state === "failed" ? `failed. ${sanitizeHumanText(input.registration.reason)}` : `${linkStates[input.registration.state]}.`;
33848
+ const account2 = input.accountEmail === null ? [
33849
+ "Account: not connected. Connect: obi login --expect-email <the user's email> (prints an address and a code for the user).",
33850
+ "Then: obi status --json"
33851
+ ] : [`Account: connected as ${sanitizeHumanText(input.accountEmail)}. Its state: obi status --json`];
33852
+ return [
33853
+ `Obi CLI ${cliVersion}, npm package @hireobi/cli. Skill links: ${links}`,
33854
+ `Agent guide, a local file in this package: ${sanitizeHumanText(join7(input.skillDirectory, "SKILL.md"))}`,
33855
+ ...account2,
33856
+ input.registration.state === "created" || input.registration.state === "present" ? "In the agent: /obi (Claude Code, Cursor), $obi (Codex) or @obi (OpenCode); /reload-skills in Claude Code if /obi is missing." : "In the agent: /obi, $obi and @obi exist once the Skill links do; until then, read the guide above.",
33857
+ 'The obi command comes only from npm i -g @hireobi/cli; the npm package named "obi" is unrelated.'
33858
+ ].join("\n") + "\n";
33859
+ }
33860
+
33861
+ // src/update-check.ts
33862
+ import { readFile as readFile9, writeFile as writeFile4 } from "node:fs/promises";
33863
+ import { join as join8 } from "node:path";
33864
+ var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
33865
+ var UPDATE_CHECK_TIMEOUT_MS = 1500;
33866
+ var releaseSchema = external_exports.string().max(32).refine((value) => parseRelease(value) !== null);
33867
+ var cacheSchema = external_exports.object({
33868
+ checkedAt: external_exports.iso.datetime(),
33869
+ latest: releaseSchema.nullable()
33870
+ });
33871
+ var cacheFile = (stateDirectory) => join8(stateDirectory, "update-check.json");
33872
+ var npmRegistry = "https://registry.npmjs.org";
33873
+ var loopbackRegistry = /^http:\/\/(127\.0\.0\.1|\[::1\]):[1-9][0-9]{0,4}$/;
33874
+ function updateCheckDisabled(env) {
33875
+ const on = (value) => value !== void 0 && value !== "" && value !== "0" && value.toLowerCase() !== "false";
33876
+ return on(env.OBI_NO_UPDATE_CHECK) || on(env.CI);
33877
+ }
33878
+ async function fetchLatestVersion(env, timeoutMs) {
33879
+ const override = env.OBI_UPDATE_REGISTRY;
33880
+ const registry2 = override !== void 0 && loopbackRegistry.test(override) ? override : npmRegistry;
33881
+ try {
33882
+ const response = await exchange({
33883
+ url: new URL("/-/package/@hireobi%2fcli/dist-tags", registry2),
33884
+ method: "GET",
33885
+ headers: { accept: "application/json" },
33886
+ timeoutMs,
33887
+ responseLimitBytes: 16 * 1024
33888
+ });
33889
+ if (response.status !== 200) return null;
33890
+ const parsed = external_exports.object({ latest: releaseSchema }).safeParse(JSON.parse(response.body));
33891
+ return parsed.success ? parsed.data.latest : null;
33892
+ } catch {
33893
+ return null;
33894
+ }
33895
+ }
33896
+ async function readCache(stateDirectory) {
33897
+ try {
33898
+ const parsed = cacheSchema.safeParse(JSON.parse(await readFile9(cacheFile(stateDirectory), "utf8")));
33899
+ return parsed.success ? parsed.data : null;
33900
+ } catch {
33901
+ return null;
33902
+ }
33903
+ }
33904
+ async function writeCache(stateDirectory, cache) {
33905
+ try {
33906
+ await ensureStateDirectory(stateDirectory);
33907
+ await writeFile4(cacheFile(stateDirectory), JSON.stringify(cache), { mode: 384 });
33908
+ } catch {
33909
+ }
33910
+ }
33911
+ async function checkForUpdate(input) {
33912
+ if (updateCheckDisabled(input.env) || parseRelease(input.installed) === null) return { state: "off" };
33913
+ try {
33914
+ const now = (input.now ?? Date.now)();
33915
+ const cache = await readCache(input.stateDirectory);
33916
+ const age = cache === null ? Infinity : now - Date.parse(cache.checkedAt);
33917
+ let latest = cache?.latest ?? null;
33918
+ let reached = true;
33919
+ if (input.fresh === true || !(age >= 0 && age < UPDATE_CHECK_INTERVAL_MS)) {
33920
+ const fetched = await (input.fetchLatest ?? ((timeoutMs) => fetchLatestVersion(input.env, timeoutMs)))(UPDATE_CHECK_TIMEOUT_MS);
33921
+ reached = fetched !== null;
33922
+ latest = fetched ?? latest;
33923
+ await writeCache(input.stateDirectory, { checkedAt: new Date(now).toISOString(), latest });
33924
+ }
33925
+ const notice = latest !== null && isOlderRelease(input.installed, latest) ? { installed: input.installed, latest, command: CLI_UPDATE_COMMAND } : null;
33926
+ return { state: "on", latest, reached, notice };
33927
+ } catch {
33928
+ return { state: "on", latest: null, reached: false, notice: null };
33929
+ }
33930
+ }
33931
+ function updateNoticeLine(notice) {
33932
+ return `Obi CLI ${notice.latest} is available (you have ${notice.installed}). Update with: ${notice.command}
33933
+ `;
33934
+ }
33935
+
33543
33936
  // src/welcome.ts
33544
33937
  var copy = {
33545
33938
  en: {
33546
33939
  tagline: "Your AI Search employee.",
33547
33940
  greeting: "Hi, I'm Obi. Let's get you discovered.",
33548
- install: "Get started: obi skill install",
33941
+ ready: "I'm in your assistants: Claude Code, Codex, Cursor and OpenCode.",
33942
+ off: "Get started: obi skill install",
33943
+ failed: "I could not join your assistants.",
33549
33944
  claude: "Claude Code / Cursor: /obi",
33550
33945
  codex: "Codex: $obi or the skills picker",
33946
+ opencode: "OpenCode: @obi",
33551
33947
  next: "Then give me your website in your assistant's chat.",
33552
33948
  help: "All commands: obi --help"
33553
33949
  },
33554
33950
  fr: {
33555
33951
  tagline: "Ton agent AI Search.",
33556
33952
  greeting: "Salut, moi c'est Obi. Faisons conna\xEEtre ton entreprise.",
33557
- install: "Pour d\xE9marrer : obi skill install",
33953
+ ready: "Je suis dans tes assistants : Claude Code, Codex, Cursor et OpenCode.",
33954
+ off: "Pour d\xE9marrer : obi skill install",
33955
+ failed: "Je n'ai pas pu rejoindre tes assistants.",
33558
33956
  claude: "Claude Code / Cursor : /obi",
33559
33957
  codex: "Codex : $obi ou le s\xE9lecteur de skills",
33958
+ opencode: "OpenCode : @obi",
33560
33959
  next: "Donne-moi ensuite ton site dans ton assistant.",
33561
33960
  help: "Toutes les commandes : obi --help"
33562
33961
  }
@@ -33610,7 +34009,7 @@ function mascotLine(row, depth) {
33610
34009
  return `${color(top, false, depth)}${color(bottom, true, depth)}\u2580`;
33611
34010
  }).join("") + reset;
33612
34011
  }
33613
- function renderWelcome(env, columns, colorDepth) {
34012
+ function renderWelcome(env, columns, colorDepth, registration) {
33614
34013
  const locale = [env.OBI_LANGUAGE, env.LC_ALL, env.LC_MESSAGES, env.LANG].find((value) => value?.trim())?.trim().toLowerCase() ?? "en";
33615
34014
  const text2 = copy[/^fr(?:[-_.@]|$)/.test(locale) ? "fr" : "en"];
33616
34015
  const colorful = columns >= 64 && colorDepth >= 8 && env.TERM !== "dumb" && !env.NO_COLOR;
@@ -33619,14 +34018,16 @@ function renderWelcome(env, columns, colorDepth) {
33619
34018
  return ` ${mascotLine(row, colorDepth)} ${aside}`;
33620
34019
  }).join("\n") : `Obi \xB7 ${cliVersion}
33621
34020
  ${text2.tagline}`;
34021
+ const skill = registration.state === "failed" ? `${text2.failed} ${sanitizeHumanText(registration.reason)}` : registration.state === "off" ? text2.off : text2.ready;
33622
34022
  return `
33623
34023
  ${title}
33624
34024
 
33625
34025
  ${text2.greeting}
33626
34026
 
33627
- ${text2.install}
34027
+ ${skill}
33628
34028
  ${text2.claude}
33629
34029
  ${text2.codex}
34030
+ ${text2.opencode}
33630
34031
 
33631
34032
  ${text2.next}
33632
34033
  ${text2.help}
@@ -33648,22 +34049,37 @@ function failureResult(error51, command, mode) {
33648
34049
  nextAction: error51.nextAction
33649
34050
  },
33650
34051
  authorization: error51.authorization,
33651
- account: error51.account
34052
+ account: error51.account,
34053
+ support: error51.support
33652
34054
  });
33653
34055
  }
33654
34056
  function markOrigin(result, origin) {
33655
34057
  return origin?.development === true ? cliResultSchema.parse({ ...result, origin: origin.origin, development: true }) : result;
33656
34058
  }
34059
+ async function updateNotice(updates) {
34060
+ const check3 = await updates;
34061
+ return check3?.state === "on" ? check3.notice : null;
34062
+ }
34063
+ function withUpdate(result, notice) {
34064
+ return notice === null ? result : cliResultSchema.parse({ ...result, update: notice });
34065
+ }
34066
+ async function announceUpdate(updates) {
34067
+ const notice = await updateNotice(updates);
34068
+ if (notice !== null) process.stderr.write(updateNoticeLine(notice));
34069
+ }
33657
34070
  async function runCli() {
33658
34071
  const argv = process.argv.slice(2);
33659
34072
  const json2 = wantsJson(argv);
33660
34073
  const welcome = argv.length === 0 && process.stdin.isTTY === true && process.stdout.isTTY === true;
34074
+ const start = argv.length === 0 && !welcome;
34075
+ const registration = await registerSkillLinks(packagedSkillDirectory(), process.env);
33661
34076
  let command = "unknown";
33662
34077
  let mode = commandMode(
33663
34078
  void 0,
33664
34079
  argv.some((value) => value === "--test-server" || value.startsWith("--test-server="))
33665
34080
  );
33666
34081
  let origin;
34082
+ let updates;
33667
34083
  let warned = false;
33668
34084
  const activeOrigin = () => {
33669
34085
  const resolved = resolveOrigin(process.env);
@@ -33675,14 +34091,24 @@ async function runCli() {
33675
34091
  return resolved;
33676
34092
  };
33677
34093
  try {
33678
- const args = parseCommandArguments(welcome ? ["--help"] : argv);
34094
+ const args = parseCommandArguments(welcome || start ? ["--help"] : argv);
33679
34095
  command = args.command;
33680
34096
  mode = commandMode(args.command, args.testServer !== void 0);
33681
34097
  origin = activeOrigin();
33682
- const result = await executeCommand(args, {
33683
- origin,
33684
- stateDirectory: await prepareStateDirectory(process.env)
33685
- });
34098
+ const stateDirectory = await prepareStateDirectory(process.env);
34099
+ updates = checkForUpdate({ env: process.env, stateDirectory, installed: cliVersion, fresh: command === "doctor" });
34100
+ const runtime = { origin, stateDirectory, updates };
34101
+ if (start) {
34102
+ const located = await locateCredential(runtime, "skip");
34103
+ process.stdout.write(renderStart({
34104
+ registration,
34105
+ skillDirectory: packagedSkillDirectory(),
34106
+ accountEmail: located === null ? null : located.credential.account.email
34107
+ }));
34108
+ await announceUpdate(updates);
34109
+ return;
34110
+ }
34111
+ const result = await executeCommand(args, runtime);
33686
34112
  const evaluationCaseId = args.evaluationCaseId;
33687
34113
  const scopeNext = (data) => ({
33688
34114
  ...data,
@@ -33694,9 +34120,11 @@ async function runCli() {
33694
34120
  ...result.ok && result.command === "status" && result.mode === "product" ? { data: scopeNext(result.data) } : result.ok && result.command === "business" && result.data.action === "start" ? { data: scopeNext(result.data) } : {}
33695
34121
  }) : result;
33696
34122
  if (scoped.ok && welcome) {
33697
- process.stdout.write(renderWelcome(process.env, process.stdout.columns, process.stdout.getColorDepth()));
34123
+ process.stdout.write(renderWelcome(process.env, process.stdout.columns, process.stdout.getColorDepth(), registration));
34124
+ await announceUpdate(updates);
33698
34125
  } else {
33699
- writeResult(markOrigin(scoped, origin), args.json);
34126
+ writeResult(withUpdate(markOrigin(scoped, origin), args.json ? await updateNotice(updates) : null), args.json);
34127
+ if (!args.json) await announceUpdate(updates);
33700
34128
  }
33701
34129
  } catch (error51) {
33702
34130
  if (origin === void 0) {
@@ -33712,7 +34140,15 @@ async function runCli() {
33712
34140
  retryable: false,
33713
34141
  nextAction: "Run the command again or inspect the local CLI installation."
33714
34142
  });
33715
- writeResult(markOrigin(failureResult(knownError, knownError.command ?? command, mode), origin), json2);
34143
+ const failure2 = failureResult(knownError, knownError.command ?? command, mode);
34144
+ if (failure2.mode !== "test" && failure2.command !== "support" && knownError.code !== "AUTHORIZATION_PENDING") {
34145
+ await prepareStateDirectory(process.env).then((directory) => recordLastError(
34146
+ directory,
34147
+ { code: knownError.code, command: failure2.command }
34148
+ ), () => void 0);
34149
+ }
34150
+ writeResult(withUpdate(markOrigin(failure2, origin), json2 ? await updateNotice(updates) : null), json2);
34151
+ if (!json2) await announceUpdate(updates);
33716
34152
  process.exitCode = knownError.exitCode;
33717
34153
  } finally {
33718
34154
  closeIdleConnections();