@hireobi/cli 0.1.8 → 0.1.9
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/README.md +10 -10
- package/dist/obi.js +383 -80
- package/package.json +1 -1
- package/skills/obi/SKILL.md +3 -1
- package/skills/obi/references/fit-and-scope.md +32 -0
- package/skills/obi/references/purchase.md +33 -2
- package/skills/obi/references/questions.md +1 -1
package/README.md
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
# Obi CLI
|
|
2
2
|
|
|
3
|
-
Obi is an AI Search Employee. This package installs the `obi` CLI and the Obi Skill, which runs inside the assistant you already use: Claude Code, Cursor or
|
|
3
|
+
Obi is an AI Search Employee. This package installs the `obi` CLI and the Obi Skill, which runs inside the assistant you already use: Claude Code, Cursor, Codex or OpenCode. The Skill does the work in the chat: public business research saved through a versioned work boundary, buyer questions, and server-measured engine answers kept apart from your assistant's own reasoning. Create your account at [agent.hireobi.com](https://agent.hireobi.com); learn more at [hireobi.com](https://hireobi.com).
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
-
Node.js 22 or later is required:
|
|
7
|
+
Node.js 22 or later is required. One line, in a terminal or pasted to your coding agent:
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm
|
|
11
|
-
obi --version
|
|
12
|
-
obi skill install
|
|
10
|
+
npm i -g @hireobi/cli && obi
|
|
13
11
|
```
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
Windows PowerShell 5.1 has no `&&`: run `npm i -g @hireobi/cli`, then `obi`.
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
The first `obi` registers the Obi Skill for Claude Code, Codex, Cursor and OpenCode, and every later command keeps it registered: two personal links, `~/.claude/skills/obi` and `~/.agents/skills/obi`. A conflicting file or link is never replaced: the command still runs, and bare `obi` says what is in the way and how to fix it. `obi skill install` registers it again after a fix and removes the links an earlier alpha of this package made under its former name; `obi skill path` locates the bundled Skill. `OBI_SKILL_LINKS=off` leaves the links to `obi skill install`. A cloud harness needs its own supported installation path.
|
|
16
|
+
|
|
17
|
+
Run by a coding agent, bare `obi` prints where the Skill's guide is and the next command. In an interactive terminal it shows the Obi mascot and a short welcome, then exits: the conversation happens in the assistant. The greeting is English or French; `OBI_LANGUAGE` overrides `LC_ALL`, `LC_MESSAGES`, then `LANG`.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
## Use
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
In [Claude Code](https://code.claude.com/docs/en/skills) or [Cursor](https://cursor.com/docs/skills), type `/obi`; in Claude Code, `/reload-skills` lists it when the session started before the install. In Codex CLI, type `$obi` or select Obi through `/skills`; in the desktop app, use the [skills picker](https://learn.chatgpt.com/docs/build-skills). In [OpenCode](https://opencode.ai/docs/skills/), type `@obi`. The Skill asks which website to work on and follows the conversation language.
|
|
22
22
|
|
|
23
23
|
Run `obi --help` for the complete command grammar and the starting steps. `--json` returns one validated envelope per invocation. After a timeout or a conflict, read the current state before retrying a write; a submission may be resent with the same file and replay key. Credentials stay in the system credential store or an explicitly selected owner-only file, and the CLI never prints tokens. The Skill's references carry the result contracts; `obi skill path` shows where they are installed.
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ An authorized operator can start an isolated public-company evaluation case with
|
|
|
26
26
|
|
|
27
27
|
## Support
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Run `obi support "<what happened>"`, write to [support@hireobi.com](mailto:support@hireobi.com) or open an issue at [github.com/hireobi/cli](https://github.com/hireobi/cli/issues).
|
|
30
30
|
|
|
31
31
|
## Repository development
|
|
32
32
|
|
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,6 +16636,34 @@ 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
|
+
|
|
16590
16667
|
// src/product-api-failures.ts
|
|
16591
16668
|
function transportError(code, message, exitCode, retryable) {
|
|
16592
16669
|
return new CliError({
|
|
@@ -16594,7 +16671,7 @@ function transportError(code, message, exitCode, retryable) {
|
|
|
16594
16671
|
message,
|
|
16595
16672
|
exitCode,
|
|
16596
16673
|
retryable,
|
|
16597
|
-
nextAction: exitCode === 6 ?
|
|
16674
|
+
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
16675
|
mode: "product"
|
|
16599
16676
|
});
|
|
16600
16677
|
}
|
|
@@ -16624,6 +16701,14 @@ function failureError(failure2, evaluationCaseId) {
|
|
|
16624
16701
|
mode: "product",
|
|
16625
16702
|
nextAction: `Run obi business show${scope} --json and read the current work before preparing a new result.`
|
|
16626
16703
|
});
|
|
16704
|
+
if (failure2.code === "MARKET_UNSUPPORTED") return new CliError({
|
|
16705
|
+
code: failure2.code,
|
|
16706
|
+
message: "Obi does not measure this market yet.",
|
|
16707
|
+
exitCode: 5,
|
|
16708
|
+
retryable: false,
|
|
16709
|
+
mode: "product",
|
|
16710
|
+
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.`
|
|
16711
|
+
});
|
|
16627
16712
|
if (failure2.code === "RATE_LIMITED") {
|
|
16628
16713
|
const wait = failure2.retryAfterSeconds === null ? "a moment" : `${failure2.retryAfterSeconds} seconds`;
|
|
16629
16714
|
return new CliError({
|
|
@@ -17213,7 +17298,7 @@ var originMarker = {
|
|
|
17213
17298
|
};
|
|
17214
17299
|
var helpDataSchema = external_exports.object({
|
|
17215
17300
|
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"]))
|
|
17301
|
+
commands: external_exports.array(external_exports.enum(["business", "capabilities", "credentials", "doctor", "eval", "harness", "login", "logout", "memory", "questions", "readiness", "research", "skill", "status", "strategies", "support"]))
|
|
17217
17302
|
});
|
|
17218
17303
|
var versionDataSchema = external_exports.object({ version: external_exports.string() });
|
|
17219
17304
|
var skillDataSchema = external_exports.discriminatedUnion("action", [
|
|
@@ -17223,6 +17308,13 @@ var skillDataSchema = external_exports.discriminatedUnion("action", [
|
|
|
17223
17308
|
var capabilitiesDataSchema = external_exports.object({
|
|
17224
17309
|
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
17310
|
version: external_exports.string(),
|
|
17311
|
+
measurementMarkets: external_exports.array(external_exports.object({ iso: external_exports.string().length(2), name: external_exports.string().min(1) })),
|
|
17312
|
+
catalog: external_exports.object({ version: external_exports.string(), currency: external_exports.literal("usd"), monthlyCents: external_exports.object({
|
|
17313
|
+
corePack: external_exports.number().int().positive(),
|
|
17314
|
+
claudePerPack: external_exports.number().int().positive(),
|
|
17315
|
+
perplexityPerPack: external_exports.number().int().positive()
|
|
17316
|
+
}) }),
|
|
17317
|
+
support: external_exports.object({ email: external_exports.literal(SUPPORT_EMAIL) }),
|
|
17226
17318
|
realLogin: external_exports.literal("available"),
|
|
17227
17319
|
paidExecution: external_exports.literal("server_authorized"),
|
|
17228
17320
|
commands: external_exports.object({
|
|
@@ -17240,7 +17332,8 @@ var capabilitiesDataSchema = external_exports.object({
|
|
|
17240
17332
|
memory: external_exports.object({ actions: external_exports.array(external_exports.string()), options: external_exports.array(external_exports.string()) }).optional(),
|
|
17241
17333
|
status: external_exports.object({ options: external_exports.array(external_exports.string()) }),
|
|
17242
17334
|
readiness: external_exports.object({ options: external_exports.array(external_exports.string()) }),
|
|
17243
|
-
strategies: external_exports.object({ options: external_exports.array(external_exports.string()) })
|
|
17335
|
+
strategies: external_exports.object({ options: external_exports.array(external_exports.string()) }),
|
|
17336
|
+
support: external_exports.object({ tags: external_exports.array(external_exports.enum(SUPPORT_TAGS)), options: external_exports.array(external_exports.string()) })
|
|
17244
17337
|
})
|
|
17245
17338
|
});
|
|
17246
17339
|
var localDoctorDataSchema = external_exports.object({
|
|
@@ -17249,6 +17342,7 @@ var localDoctorDataSchema = external_exports.object({
|
|
|
17249
17342
|
origin: external_exports.string(),
|
|
17250
17343
|
credentialStorage: external_exports.union([backendKindSchema, external_exports.literal("none")]),
|
|
17251
17344
|
videoTools: external_exports.object({ ytDlp: external_exports.boolean(), ffmpeg: external_exports.boolean() }),
|
|
17345
|
+
server: external_exports.enum(["reachable", "degraded", "unreachable"]),
|
|
17252
17346
|
diagnostic: external_exports.literal("completed")
|
|
17253
17347
|
});
|
|
17254
17348
|
var loginDataSchema = external_exports.object({
|
|
@@ -17545,6 +17639,7 @@ var successfulResultSchema = external_exports.union([
|
|
|
17545
17639
|
external_exports.object({ ...envelope2, command: external_exports.literal("harness"), mode: external_exports.literal("product"), data: harnessCoverageDataSchema }),
|
|
17546
17640
|
...harnessEvidenceDataSchema.map((schema) => external_exports.object({ ...envelope2, command: external_exports.literal("harness"), mode: external_exports.literal("product"), data: schema })),
|
|
17547
17641
|
external_exports.object({ ...envelope2, command: external_exports.literal("harness"), mode: external_exports.literal("product"), data: harnessEvidenceExportDataSchema }),
|
|
17642
|
+
external_exports.object({ ...envelope2, command: external_exports.literal("support"), mode: external_exports.literal("product"), data: supportDataSchema }),
|
|
17548
17643
|
external_exports.object({ ...envelope2, command: external_exports.literal("memory"), mode: external_exports.literal("product"), data: memoryListDataSchema }),
|
|
17549
17644
|
external_exports.object({ ...envelope2, command: external_exports.literal("memory"), mode: external_exports.literal("product"), data: memoryChangeDataSchema }),
|
|
17550
17645
|
external_exports.object({
|
|
@@ -17595,7 +17690,8 @@ var failedResultSchema = external_exports.object({
|
|
|
17595
17690
|
...originMarker,
|
|
17596
17691
|
error: errorSchema,
|
|
17597
17692
|
authorization: pendingAuthorizationViewSchema.optional(),
|
|
17598
|
-
account: confirmedAccountSchema.optional()
|
|
17693
|
+
account: confirmedAccountSchema.optional(),
|
|
17694
|
+
support: supportDataSchema.optional()
|
|
17599
17695
|
});
|
|
17600
17696
|
var cliResultSchema = external_exports.union([successfulResultSchema, failedResultSchema]);
|
|
17601
17697
|
|
|
@@ -18060,6 +18156,9 @@ async function readReadiness(context, accessToken) {
|
|
|
18060
18156
|
responseLimitBytes: BUSINESS_REVIEW_RESPONSE_LIMIT_BYTES
|
|
18061
18157
|
}), ProductReadinessResponseSchema);
|
|
18062
18158
|
}
|
|
18159
|
+
async function sendSupportMessage(context, accessToken, message) {
|
|
18160
|
+
return interpretReview(await send(context, { method: "POST", path: "/api/v1/support", bearer: accessToken, body: SupportMessageSchema.parse(message) }), SupportMessageResponseSchema);
|
|
18161
|
+
}
|
|
18063
18162
|
|
|
18064
18163
|
// src/refresh.ts
|
|
18065
18164
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
@@ -19210,7 +19309,7 @@ async function submitHarnessObservation(context, path) {
|
|
|
19210
19309
|
|
|
19211
19310
|
// src/commands.ts
|
|
19212
19311
|
import { access } from "node:fs/promises";
|
|
19213
|
-
import { fileURLToPath } from "node:url";
|
|
19312
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
19214
19313
|
|
|
19215
19314
|
// src/onboarding-guide.ts
|
|
19216
19315
|
var READING_POLL_MS = 6e4;
|
|
@@ -20275,6 +20374,111 @@ function memoryText(data) {
|
|
|
20275
20374
|
`;
|
|
20276
20375
|
}
|
|
20277
20376
|
|
|
20377
|
+
// src/support-commands.ts
|
|
20378
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
20379
|
+
import { release } from "node:os";
|
|
20380
|
+
|
|
20381
|
+
// src/last-error.ts
|
|
20382
|
+
import { readFile as readFile6, writeFile as writeFile3 } from "node:fs/promises";
|
|
20383
|
+
import { join as join4 } from "node:path";
|
|
20384
|
+
var fileIn = (stateDirectory) => join4(stateDirectory, "last-error.json");
|
|
20385
|
+
async function recordLastError(stateDirectory, error51) {
|
|
20386
|
+
try {
|
|
20387
|
+
const parsed = lastErrorSchema.safeParse({ ...error51, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
20388
|
+
if (!parsed.success) return;
|
|
20389
|
+
await ensureStateDirectory(stateDirectory);
|
|
20390
|
+
await writeFile3(fileIn(stateDirectory), JSON.stringify(parsed.data), { mode: 384 });
|
|
20391
|
+
} catch {
|
|
20392
|
+
}
|
|
20393
|
+
}
|
|
20394
|
+
async function readLastError(stateDirectory) {
|
|
20395
|
+
try {
|
|
20396
|
+
const parsed = lastErrorSchema.safeParse(JSON.parse(await readFile6(fileIn(stateDirectory), "utf8")));
|
|
20397
|
+
return parsed.success ? parsed.data : null;
|
|
20398
|
+
} catch {
|
|
20399
|
+
return null;
|
|
20400
|
+
}
|
|
20401
|
+
}
|
|
20402
|
+
|
|
20403
|
+
// src/version.ts
|
|
20404
|
+
var cliVersion = "0.1.9";
|
|
20405
|
+
|
|
20406
|
+
// src/support-commands.ts
|
|
20407
|
+
async function probeServer(origin, timeoutMs) {
|
|
20408
|
+
try {
|
|
20409
|
+
const response = await exchange({
|
|
20410
|
+
url: new URL("/api/health", origin.origin),
|
|
20411
|
+
method: "GET",
|
|
20412
|
+
headers: { accept: "application/json" },
|
|
20413
|
+
timeoutMs
|
|
20414
|
+
});
|
|
20415
|
+
return response.status === 200 ? "reachable" : "degraded";
|
|
20416
|
+
} catch {
|
|
20417
|
+
return "unreachable";
|
|
20418
|
+
}
|
|
20419
|
+
}
|
|
20420
|
+
async function sendSupport(context, input) {
|
|
20421
|
+
const lastError = await readLastError(context.stateDirectory);
|
|
20422
|
+
const diagnostic = { cliVersion, node: process.version, os: `${process.platform} ${release()} ${process.arch}`, lastError };
|
|
20423
|
+
let account2 = null;
|
|
20424
|
+
let reason = null;
|
|
20425
|
+
try {
|
|
20426
|
+
const located = await locateCredential(context);
|
|
20427
|
+
if (located === null) {
|
|
20428
|
+
reason = "AUTH_REQUIRED";
|
|
20429
|
+
} else {
|
|
20430
|
+
account2 = located.credential.account.email;
|
|
20431
|
+
const requestId = createHash2("sha256").update(JSON.stringify([
|
|
20432
|
+
context.origin.origin,
|
|
20433
|
+
account2,
|
|
20434
|
+
input.message,
|
|
20435
|
+
input.tag,
|
|
20436
|
+
(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
20437
|
+
])).digest("hex").slice(0, 32);
|
|
20438
|
+
const { result } = await useCredential(
|
|
20439
|
+
context,
|
|
20440
|
+
located,
|
|
20441
|
+
(token) => sendSupportMessage(context, token, { requestId, ...input, diagnostic })
|
|
20442
|
+
);
|
|
20443
|
+
if (!result.ok) reason = result.failure.httpStatus >= 500 ? "SERVICE_UNAVAILABLE" : result.failure.code;
|
|
20444
|
+
}
|
|
20445
|
+
} catch (error51) {
|
|
20446
|
+
reason = error51 instanceof CliError ? error51.code : "INTERNAL_ERROR";
|
|
20447
|
+
}
|
|
20448
|
+
const diagnosticText = [
|
|
20449
|
+
`Message: ${input.message}`,
|
|
20450
|
+
`Tag: ${input.tag ?? "general"}`,
|
|
20451
|
+
`Obi CLI ${diagnostic.cliVersion} \xB7 Node ${diagnostic.node} \xB7 ${diagnostic.os}`,
|
|
20452
|
+
`Last error: ${lastError === null ? "none" : `${lastError.code} (${lastError.command}) at ${lastError.at}`}`,
|
|
20453
|
+
`Account: ${account2 ?? "not signed in"}`
|
|
20454
|
+
].join("\n");
|
|
20455
|
+
const data = {
|
|
20456
|
+
delivery: reason === null ? "sent" : "not_sent",
|
|
20457
|
+
reason,
|
|
20458
|
+
supportEmail: SUPPORT_EMAIL,
|
|
20459
|
+
mailto: `mailto:${SUPPORT_EMAIL}`,
|
|
20460
|
+
tag: input.tag,
|
|
20461
|
+
account: account2,
|
|
20462
|
+
origin: context.origin.origin,
|
|
20463
|
+
diagnosticText
|
|
20464
|
+
};
|
|
20465
|
+
if (reason === null) return data;
|
|
20466
|
+
throw new CliError({
|
|
20467
|
+
code: "SUPPORT_NOT_SENT",
|
|
20468
|
+
message: `The message was not sent to Obi support (${reason}).`,
|
|
20469
|
+
exitCode: reason === "AUTH_REQUIRED" ? 3 : 6,
|
|
20470
|
+
retryable: reason !== "AUTH_REQUIRED",
|
|
20471
|
+
mode: "product",
|
|
20472
|
+
command: "support",
|
|
20473
|
+
nextAction: `Write to ${SUPPORT_EMAIL} and paste the block below (support.diagnosticText with --json). Never add a token or a credential file.`,
|
|
20474
|
+
support: data
|
|
20475
|
+
});
|
|
20476
|
+
}
|
|
20477
|
+
function supportText(data) {
|
|
20478
|
+
return `Sent to Obi support (${data.supportEmail}). The reply goes to ${sanitizeHumanText(data.account ?? "the account email")}.
|
|
20479
|
+
`;
|
|
20480
|
+
}
|
|
20481
|
+
|
|
20278
20482
|
// src/scan-output.ts
|
|
20279
20483
|
function heading(data) {
|
|
20280
20484
|
const scored = `${data.scoredCells}/${data.expectedCells} answers scored from question version ${data.candidateVersion ?? "none"}.`;
|
|
@@ -20399,6 +20603,7 @@ function failureText(result) {
|
|
|
20399
20603
|
lines.push(`Approving account: ${sanitizeHumanText(result.account.email)} (${result.account.role}).`);
|
|
20400
20604
|
}
|
|
20401
20605
|
lines.push(sanitizeHumanText(result.error.nextAction));
|
|
20606
|
+
if (result.support !== void 0) lines.push("", ...result.support.diagnosticText.split("\n").map(sanitizeHumanText));
|
|
20402
20607
|
return `${lines.join("\n")}
|
|
20403
20608
|
`;
|
|
20404
20609
|
}
|
|
@@ -20441,11 +20646,12 @@ function toHumanOutput(result) {
|
|
|
20441
20646
|
return `Obi \xB7 AI Search Employee
|
|
20442
20647
|
|
|
20443
20648
|
Get started
|
|
20444
|
-
1.
|
|
20445
|
-
2. In a new Claude Code or Cursor chat, type /obi.
|
|
20649
|
+
1. In Claude Code or Cursor, type /obi.
|
|
20446
20650
|
In Codex CLI, type $obi or choose Obi from /skills.
|
|
20447
20651
|
In the desktop app, select Obi from the skills picker.
|
|
20448
|
-
|
|
20652
|
+
In OpenCode, type @obi.
|
|
20653
|
+
2. Give Obi a website or ask to continue saved work.
|
|
20654
|
+
Every obi command registers the Skill; obi skill install repairs it.
|
|
20449
20655
|
|
|
20450
20656
|
Commands
|
|
20451
20657
|
${usage2}
|
|
@@ -20457,11 +20663,11 @@ ${usage2}
|
|
|
20457
20663
|
}
|
|
20458
20664
|
if (result.command === "skill") {
|
|
20459
20665
|
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.
|
|
20666
|
+
` : `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
20667
|
${result.data.removed.length > 0 ? "Earlier Skill links of this package under another name were removed; use /obi.\n" : ""}`;
|
|
20462
20668
|
}
|
|
20463
20669
|
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";
|
|
20670
|
+
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
20671
|
}
|
|
20466
20672
|
if (result.command === "eval") {
|
|
20467
20673
|
if ("evaluation" in result.data) {
|
|
@@ -20476,6 +20682,7 @@ Next: ${sanitizeHumanText(result.data.nextStep)}
|
|
|
20476
20682
|
return result.data.action === "list" ? connectionListText(result.data) : revocationText(result.data);
|
|
20477
20683
|
}
|
|
20478
20684
|
if (result.command === "memory") return memoryText(result.data);
|
|
20685
|
+
if (result.command === "support") return supportText(result.data);
|
|
20479
20686
|
if (result.command === "doctor") {
|
|
20480
20687
|
if (result.mode === "test") {
|
|
20481
20688
|
return `Synthetic test protocol reachable. State: ${String(result.data.state)}.
|
|
@@ -20483,7 +20690,12 @@ Next: ${sanitizeHumanText(result.data.nextStep)}
|
|
|
20483
20690
|
}
|
|
20484
20691
|
const storage = result.data.credentialStorage === "none" ? "no stored connection" : `connection stored in ${storageLabels[result.data.credentialStorage]}`;
|
|
20485
20692
|
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
|
-
|
|
20693
|
+
const server = {
|
|
20694
|
+
reachable: "Obi server reachable.",
|
|
20695
|
+
degraded: `Obi server degraded: try again later, and write to ${SUPPORT_EMAIL} if it lasts.`,
|
|
20696
|
+
unreachable: `Obi server unreachable: check the network connection, and write to ${SUPPORT_EMAIL} if it lasts.`
|
|
20697
|
+
}[result.data.server];
|
|
20698
|
+
return `Local diagnostic completed for ${sanitizeHumanText(result.data.origin)}: ${storage}; ${video}. ${server}
|
|
20487
20699
|
`;
|
|
20488
20700
|
}
|
|
20489
20701
|
if (result.command === "login") {
|
|
@@ -20819,9 +21031,6 @@ async function confirmAccount(account2) {
|
|
|
20819
21031
|
}
|
|
20820
21032
|
}
|
|
20821
21033
|
|
|
20822
|
-
// src/version.ts
|
|
20823
|
-
var cliVersion = "0.1.8";
|
|
20824
|
-
|
|
20825
21034
|
// src/login.ts
|
|
20826
21035
|
var loginRequestTimeoutMs = 1e4;
|
|
20827
21036
|
var maxLinkLength = 200;
|
|
@@ -21031,7 +21240,7 @@ function accountMismatch(account2, revoked, expected) {
|
|
|
21031
21240
|
}
|
|
21032
21241
|
async function confirmAndStore(context, store, pair, receivedAt, expectedEmail) {
|
|
21033
21242
|
const api = context;
|
|
21034
|
-
const
|
|
21243
|
+
const release3 = guardNewFamily(api, pair);
|
|
21035
21244
|
let stored = false;
|
|
21036
21245
|
try {
|
|
21037
21246
|
const status = await readAccountStatus(api, pair.accessToken);
|
|
@@ -21062,7 +21271,7 @@ async function confirmAndStore(context, store, pair, receivedAt, expectedEmail)
|
|
|
21062
21271
|
}
|
|
21063
21272
|
await store.writeCredential(credentialFromPair(context.origin.origin, pair, confirmed, receivedAt));
|
|
21064
21273
|
stored = true;
|
|
21065
|
-
|
|
21274
|
+
release3();
|
|
21066
21275
|
});
|
|
21067
21276
|
try {
|
|
21068
21277
|
await store.removePending();
|
|
@@ -21082,7 +21291,7 @@ async function confirmAndStore(context, store, pair, receivedAt, expectedEmail)
|
|
|
21082
21291
|
}
|
|
21083
21292
|
throw error51;
|
|
21084
21293
|
} finally {
|
|
21085
|
-
|
|
21294
|
+
release3();
|
|
21086
21295
|
}
|
|
21087
21296
|
}
|
|
21088
21297
|
async function login(session, options) {
|
|
@@ -21221,12 +21430,12 @@ async function proceedToFirstScan(context, candidateVersion) {
|
|
|
21221
21430
|
}
|
|
21222
21431
|
|
|
21223
21432
|
// src/research-commands.ts
|
|
21224
|
-
import { readFile as
|
|
21433
|
+
import { readFile as readFile7, stat as stat6 } from "node:fs/promises";
|
|
21225
21434
|
import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
|
|
21226
21435
|
import { resolve as resolve2 } from "node:path";
|
|
21227
21436
|
|
|
21228
21437
|
// ../geo-engine/src/geo/host-acquisition.ts
|
|
21229
|
-
import { createHash as
|
|
21438
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
21230
21439
|
|
|
21231
21440
|
// ../geo-engine/src/url-safety.ts
|
|
21232
21441
|
var PRIVATE_IP_RE = /^(127\.|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.|0\.)/;
|
|
@@ -21397,7 +21606,7 @@ async function safeFetch(url3, opts = {}) {
|
|
|
21397
21606
|
if (!REDIRECT_STATUSES.has(res.status)) return res;
|
|
21398
21607
|
const location = res.headers.get("location");
|
|
21399
21608
|
if (!location) return res;
|
|
21400
|
-
await
|
|
21609
|
+
await release2(res);
|
|
21401
21610
|
let next;
|
|
21402
21611
|
try {
|
|
21403
21612
|
next = new URL(location, current).href;
|
|
@@ -21408,7 +21617,7 @@ async function safeFetch(url3, opts = {}) {
|
|
|
21408
21617
|
}
|
|
21409
21618
|
throw new TooManyRedirectsError(url3, maxRedirects);
|
|
21410
21619
|
}
|
|
21411
|
-
async function
|
|
21620
|
+
async function release2(res) {
|
|
21412
21621
|
await res.body?.cancel().catch((err) => {
|
|
21413
21622
|
console.warn("[safe-fetch] cancelling an unread body failed:", err);
|
|
21414
21623
|
});
|
|
@@ -21416,7 +21625,7 @@ async function release(res) {
|
|
|
21416
21625
|
async function readCapped(res, maxBytes = MAX_RESPONSE_BYTES) {
|
|
21417
21626
|
const declared = Number(res.headers.get("content-length"));
|
|
21418
21627
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
21419
|
-
await
|
|
21628
|
+
await release2(res);
|
|
21420
21629
|
return "";
|
|
21421
21630
|
}
|
|
21422
21631
|
if (!res.body) return (await res.text()).slice(0, maxBytes);
|
|
@@ -21582,7 +21791,7 @@ function createVettedTransport(options = {}) {
|
|
|
21582
21791
|
}
|
|
21583
21792
|
|
|
21584
21793
|
// ../geo-engine/src/geo/page-evidence.ts
|
|
21585
|
-
import { createHash as
|
|
21794
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
21586
21795
|
|
|
21587
21796
|
// ../node_modules/parse5/dist/common/unicode.js
|
|
21588
21797
|
var UNDEFINED_CODE_POINTS = /* @__PURE__ */ new Set([
|
|
@@ -29502,7 +29711,7 @@ function* descendants(node) {
|
|
|
29502
29711
|
}
|
|
29503
29712
|
}
|
|
29504
29713
|
function sha256(s) {
|
|
29505
|
-
return
|
|
29714
|
+
return createHash3("sha256").update(s).digest("hex");
|
|
29506
29715
|
}
|
|
29507
29716
|
var SKIPPED = /* @__PURE__ */ new Set(["script", "style", "noscript", "template", "svg", "iframe", "object", "canvas", "select", "head"]);
|
|
29508
29717
|
var HEADING = /^h([1-6])$/;
|
|
@@ -29793,7 +30002,7 @@ function headingTexts(html, level) {
|
|
|
29793
30002
|
}
|
|
29794
30003
|
|
|
29795
30004
|
// ../geo-engine/src/geo/selected-page-evidence.ts
|
|
29796
|
-
import { createHash as
|
|
30005
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
29797
30006
|
|
|
29798
30007
|
// ../geo-engine/src/geo/material-fact-contract.ts
|
|
29799
30008
|
var text = external_exports.string().trim().min(1);
|
|
@@ -29899,10 +30108,10 @@ var SelectedPageEvidenceSchema = external_exports.strictObject({
|
|
|
29899
30108
|
function selectedPageId(pageUrl) {
|
|
29900
30109
|
const selected = new URL(pageUrl);
|
|
29901
30110
|
selected.hash = "";
|
|
29902
|
-
return `page:${
|
|
30111
|
+
return `page:${createHash4("sha256").update(selected.href).digest("hex")}`;
|
|
29903
30112
|
}
|
|
29904
30113
|
function observationBodyHash(body) {
|
|
29905
|
-
return
|
|
30114
|
+
return createHash4("sha256").update(body).digest("hex");
|
|
29906
30115
|
}
|
|
29907
30116
|
function sameSelectedHost(selectedUrl, finalUrl) {
|
|
29908
30117
|
const host = (value) => new URL(value).hostname.toLowerCase().replace(/^www\./, "");
|
|
@@ -30354,14 +30563,14 @@ function evaluateAccess(parsed, token, url3, fallbackTokens = []) {
|
|
|
30354
30563
|
}
|
|
30355
30564
|
|
|
30356
30565
|
// ../geo-engine/src/geo/readiness/findings.ts
|
|
30357
|
-
import { createHash as
|
|
30566
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
30358
30567
|
function issueId(origin, checkId, scopeKey) {
|
|
30359
|
-
return
|
|
30568
|
+
return createHash5("sha256").update(`${origin.toLowerCase()}
|
|
30360
30569
|
${checkId}
|
|
30361
30570
|
${scopeKey}`).digest("hex").slice(0, 16);
|
|
30362
30571
|
}
|
|
30363
30572
|
function fingerprint(body) {
|
|
30364
|
-
return body === null ? null :
|
|
30573
|
+
return body === null ? null : createHash5("sha256").update(body).digest("hex");
|
|
30365
30574
|
}
|
|
30366
30575
|
function makeFinding({ origin, scopeKey, policyChoice = false, materiality = "site", ...rest }) {
|
|
30367
30576
|
return { id: issueId(origin, rest.checkId, scopeKey), policyChoice, materiality, ...rest };
|
|
@@ -30864,7 +31073,7 @@ function validateState(raw) {
|
|
|
30864
31073
|
return state;
|
|
30865
31074
|
}
|
|
30866
31075
|
function matchesFingerprint(observation) {
|
|
30867
|
-
return observation.body === null ? observation.bodyHash === null : observation.bodyHash ===
|
|
31076
|
+
return observation.body === null ? observation.bodyHash === null : observation.bodyHash === createHash6("sha256").update(observation.body).digest("hex");
|
|
30868
31077
|
}
|
|
30869
31078
|
function pendingProbeRequests(state) {
|
|
30870
31079
|
const home = state.resources.find((resource) => resource.kind === "page" && resource.url === `${state.origin}/`);
|
|
@@ -32635,7 +32844,7 @@ async function readJson(path, maxBytes = maxCorpusBytes) {
|
|
|
32635
32844
|
try {
|
|
32636
32845
|
const info = await stat6(path);
|
|
32637
32846
|
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
|
|
32847
|
+
const content = await readFile7(path);
|
|
32639
32848
|
if (content.byteLength > maxBytes) throw fileError(`The research file exceeds the ${maxBytes / 1024 / 1024} MiB local limit.`);
|
|
32640
32849
|
return JSON.parse(content.toString("utf8"));
|
|
32641
32850
|
} catch (error51) {
|
|
@@ -32823,8 +33032,8 @@ async function captureResearch(input, checkpoint, output) {
|
|
|
32823
33032
|
|
|
32824
33033
|
// src/research-video.ts
|
|
32825
33034
|
import { spawn as spawn2 } from "node:child_process";
|
|
32826
|
-
import { mkdir as mkdir2, readdir as readdir3, readFile as
|
|
32827
|
-
import { join as
|
|
33035
|
+
import { mkdir as mkdir2, readdir as readdir3, readFile as readFile8, rm as rm2 } from "node:fs/promises";
|
|
33036
|
+
import { join as join5, resolve as resolve3 } from "node:path";
|
|
32828
33037
|
var maxToolOutputBytes = 256 * 1024;
|
|
32829
33038
|
var runTool = (executable, args, timeoutMs) => new Promise((done, fail) => {
|
|
32830
33039
|
const child = spawn2(executable, [...args], { stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
@@ -32923,12 +33132,12 @@ async function readVideo(rawUrl, outputDir, frames, runner = runTool) {
|
|
|
32923
33132
|
const directory = resolve3(outputDir);
|
|
32924
33133
|
await mkdir2(directory, { recursive: true });
|
|
32925
33134
|
const base = ["--ignore-config", "--no-playlist", "--quiet", "--no-warnings"];
|
|
32926
|
-
const target = ["-o",
|
|
33135
|
+
const target = ["-o", join5(templateSafe(directory), "video.%(ext)s")];
|
|
32927
33136
|
const failed = (message, nextAction) => videoError("RESEARCH_VIDEO_FAILED", message, nextAction);
|
|
32928
33137
|
const metadata = await runner("yt-dlp", [...base, "--skip-download", "--write-info-json", ...target, url3.href], 18e4).catch((error51) => {
|
|
32929
33138
|
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
33139
|
});
|
|
32931
|
-
const info = metadata.exitCode === 0 ? JSON.parse(await
|
|
33140
|
+
const info = metadata.exitCode === 0 ? JSON.parse(await readFile8(join5(directory, "video.info.json"), "utf8")) : null;
|
|
32932
33141
|
if (!info) {
|
|
32933
33142
|
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
33143
|
}
|
|
@@ -32950,11 +33159,11 @@ async function readVideo(rawUrl, outputDir, frames, runner = runTool) {
|
|
|
32950
33159
|
if (subtitles?.exitCode !== 0 || !file2) {
|
|
32951
33160
|
throw failed("The video has captions but their download failed.", "Retry in a minute; the platform may be rate-limiting.");
|
|
32952
33161
|
}
|
|
32953
|
-
segments = parseVtt(await
|
|
33162
|
+
segments = parseVtt(await readFile8(join5(directory, file2), "utf8"));
|
|
32954
33163
|
}
|
|
32955
33164
|
const duration3 = typeof info.duration === "number" ? info.duration : null;
|
|
32956
33165
|
const frameFiles = [];
|
|
32957
|
-
const output =
|
|
33166
|
+
const output = join5(directory, "transcript.json");
|
|
32958
33167
|
const save = () => writeJsonAtomic(
|
|
32959
33168
|
output,
|
|
32960
33169
|
{
|
|
@@ -32974,7 +33183,7 @@ async function readVideo(rawUrl, outputDir, frames, runner = runTool) {
|
|
|
32974
33183
|
throw failed("The video's length is unknown, so frames cannot be spaced.", "Retry with --frames 0 to keep the captions only.");
|
|
32975
33184
|
}
|
|
32976
33185
|
if (frames > 0 && duration3) {
|
|
32977
|
-
const removeMedia = async () => Promise.all((await readdir3(directory)).filter((file2) => file2.startsWith("media.")).map((file2) => rm2(
|
|
33186
|
+
const removeMedia = async () => Promise.all((await readdir3(directory)).filter((file2) => file2.startsWith("media.")).map((file2) => rm2(join5(directory, file2), { force: true })));
|
|
32978
33187
|
await removeMedia();
|
|
32979
33188
|
try {
|
|
32980
33189
|
const download = await runner("yt-dlp", [
|
|
@@ -32982,7 +33191,7 @@ async function readVideo(rawUrl, outputDir, frames, runner = runTool) {
|
|
|
32982
33191
|
"-f",
|
|
32983
33192
|
"b[height<=480]/bv*[height<=480]/w",
|
|
32984
33193
|
"-o",
|
|
32985
|
-
|
|
33194
|
+
join5(templateSafe(directory), "media.%(ext)s"),
|
|
32986
33195
|
url3.href
|
|
32987
33196
|
], 6e5).catch(() => {
|
|
32988
33197
|
throw failed("The video download did not finish.", "Retry with --frames 0 to keep the captions only.");
|
|
@@ -33002,14 +33211,14 @@ async function readVideo(rawUrl, outputDir, frames, runner = runTool) {
|
|
|
33002
33211
|
"-ss",
|
|
33003
33212
|
String(atSeconds),
|
|
33004
33213
|
"-i",
|
|
33005
|
-
|
|
33214
|
+
join5(directory, media),
|
|
33006
33215
|
"-frames:v",
|
|
33007
33216
|
"1",
|
|
33008
33217
|
"-vf",
|
|
33009
33218
|
"scale=1024:-2",
|
|
33010
|
-
|
|
33219
|
+
join5(directory, file2)
|
|
33011
33220
|
], 6e4).catch(() => null);
|
|
33012
|
-
if (extracted?.exitCode === 0) frameFiles.push({ file:
|
|
33221
|
+
if (extracted?.exitCode === 0) frameFiles.push({ file: join5(directory, file2), atSeconds });
|
|
33013
33222
|
}
|
|
33014
33223
|
} finally {
|
|
33015
33224
|
await removeMedia();
|
|
@@ -33128,13 +33337,15 @@ async function showAlphaStrategies(context) {
|
|
|
33128
33337
|
// src/skill-install.ts
|
|
33129
33338
|
import { lstat as lstat3, mkdir as mkdir3, readlink, realpath, symlink, unlink as unlink5 } from "node:fs/promises";
|
|
33130
33339
|
import { homedir as homedir2 } from "node:os";
|
|
33131
|
-
import { join as
|
|
33340
|
+
import { join as join6, sep } from "node:path";
|
|
33341
|
+
import { fileURLToPath } from "node:url";
|
|
33132
33342
|
var skillDirectories = [".agents/skills", ".claude/skills"];
|
|
33133
33343
|
var alphaSkillName = "heysurface";
|
|
33344
|
+
var messageLimit = 300;
|
|
33134
33345
|
function installError(message) {
|
|
33135
33346
|
return new CliError({
|
|
33136
33347
|
code: "SKILL_INSTALL_FAILED",
|
|
33137
|
-
message,
|
|
33348
|
+
message: message.length > messageLimit ? `${message.slice(0, messageLimit - 1)}\u2026` : message,
|
|
33138
33349
|
exitCode: 4,
|
|
33139
33350
|
retryable: false,
|
|
33140
33351
|
nextAction: "Check the personal Skill directories and run obi skill install again. Existing files are never replaced.",
|
|
@@ -33164,7 +33375,7 @@ async function linksIntoPackage(path, packageRoot) {
|
|
|
33164
33375
|
return target === packageRoot || target.startsWith(`${packageRoot}${sep}`);
|
|
33165
33376
|
} catch {
|
|
33166
33377
|
try {
|
|
33167
|
-
return await readlink(path) ===
|
|
33378
|
+
return await readlink(path) === join6(packageRoot, "skills", alphaSkillName);
|
|
33168
33379
|
} catch {
|
|
33169
33380
|
return false;
|
|
33170
33381
|
}
|
|
@@ -33173,37 +33384,70 @@ async function linksIntoPackage(path, packageRoot) {
|
|
|
33173
33384
|
async function installSkill(sourceDirectory) {
|
|
33174
33385
|
const source = await realpath(sourceDirectory);
|
|
33175
33386
|
const home = homedir2();
|
|
33176
|
-
const destinations = skillDirectories.map((directory) =>
|
|
33387
|
+
const destinations = skillDirectories.map((directory) => join6(home, directory, "obi"));
|
|
33388
|
+
const registered = async (destination) => {
|
|
33389
|
+
try {
|
|
33390
|
+
return await realpath(destination) === source;
|
|
33391
|
+
} catch {
|
|
33392
|
+
return false;
|
|
33393
|
+
}
|
|
33394
|
+
};
|
|
33177
33395
|
for (const destination of destinations) {
|
|
33178
|
-
if (!await existingPath(destination)) continue;
|
|
33396
|
+
if (!await existingPath(destination) || await registered(destination)) continue;
|
|
33397
|
+
let target = null;
|
|
33179
33398
|
try {
|
|
33180
|
-
if (await
|
|
33399
|
+
if ((await lstat3(destination)).isSymbolicLink()) target = await readlink(destination);
|
|
33181
33400
|
} catch {
|
|
33401
|
+
continue;
|
|
33182
33402
|
}
|
|
33183
|
-
throw installError(`A Skill entry already exists at ${destination}
|
|
33403
|
+
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
33404
|
}
|
|
33185
33405
|
for (const destination of destinations) {
|
|
33186
|
-
if (await
|
|
33406
|
+
if (await registered(destination)) continue;
|
|
33187
33407
|
try {
|
|
33188
|
-
await mkdir3(
|
|
33408
|
+
await mkdir3(join6(destination, ".."), { recursive: true });
|
|
33189
33409
|
await symlink(source, destination, process.platform === "win32" ? "junction" : "dir");
|
|
33190
33410
|
} catch {
|
|
33191
|
-
|
|
33411
|
+
if (await registered(destination)) continue;
|
|
33412
|
+
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
33413
|
}
|
|
33193
33414
|
}
|
|
33194
33415
|
const removed = [];
|
|
33195
|
-
const packageRoot = await realpath(
|
|
33196
|
-
for (const alpha of skillDirectories.map((directory) =>
|
|
33416
|
+
const packageRoot = await realpath(join6(source, "..", ".."));
|
|
33417
|
+
for (const alpha of skillDirectories.map((directory) => join6(home, directory, alphaSkillName))) {
|
|
33197
33418
|
if (!await linksIntoPackage(alpha, packageRoot)) continue;
|
|
33198
33419
|
try {
|
|
33199
33420
|
await unlink5(alpha);
|
|
33200
|
-
} catch {
|
|
33421
|
+
} catch (error51) {
|
|
33422
|
+
if (error51.code === "ENOENT") continue;
|
|
33201
33423
|
throw installError(`Could not retire the earlier Skill link at ${alpha}.`);
|
|
33202
33424
|
}
|
|
33203
33425
|
removed.push(alpha);
|
|
33204
33426
|
}
|
|
33205
33427
|
return { paths: destinations, removed };
|
|
33206
33428
|
}
|
|
33429
|
+
function packagedSkillDirectory() {
|
|
33430
|
+
return fileURLToPath(new URL("../skills/obi", import.meta.url));
|
|
33431
|
+
}
|
|
33432
|
+
async function registerSkillLinks(sourceDirectory, env) {
|
|
33433
|
+
if (env.OBI_SKILL_LINKS === "off") return { state: "off" };
|
|
33434
|
+
try {
|
|
33435
|
+
const source = await realpath(sourceDirectory);
|
|
33436
|
+
let present3 = true;
|
|
33437
|
+
for (const directory of skillDirectories) {
|
|
33438
|
+
try {
|
|
33439
|
+
if (await realpath(join6(homedir2(), directory, "obi")) !== source) present3 = false;
|
|
33440
|
+
} catch {
|
|
33441
|
+
present3 = false;
|
|
33442
|
+
}
|
|
33443
|
+
}
|
|
33444
|
+
if (present3) return { state: "present" };
|
|
33445
|
+
await installSkill(sourceDirectory);
|
|
33446
|
+
return { state: "created" };
|
|
33447
|
+
} catch (error51) {
|
|
33448
|
+
return { state: "failed", reason: error51 instanceof CliError ? error51.message : "The Obi Skill could not be registered." };
|
|
33449
|
+
}
|
|
33450
|
+
}
|
|
33207
33451
|
|
|
33208
33452
|
// src/transport.ts
|
|
33209
33453
|
var loopbackOrigin = /^http:\/\/(127\.0\.0\.1|\[::1\]):([1-9][0-9]{0,4})$/;
|
|
@@ -33328,7 +33572,7 @@ async function fetchTestStatus(origin, timeoutMs) {
|
|
|
33328
33572
|
}
|
|
33329
33573
|
|
|
33330
33574
|
// src/commands.ts
|
|
33331
|
-
var usage =
|
|
33575
|
+
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
33576
|
function success2(command, mode, data) {
|
|
33333
33577
|
return cliResultSchema.parse({ schemaVersion: 1, command, mode, ok: true, data });
|
|
33334
33578
|
}
|
|
@@ -33355,7 +33599,7 @@ function statusResult(status) {
|
|
|
33355
33599
|
}
|
|
33356
33600
|
return success2("status", "test", status);
|
|
33357
33601
|
}
|
|
33358
|
-
async function localDoctor(runtime) {
|
|
33602
|
+
async function localDoctor(runtime, timeoutMs) {
|
|
33359
33603
|
const located = await locateCredential(runtime);
|
|
33360
33604
|
return success2("doctor", "local", {
|
|
33361
33605
|
node: process.version,
|
|
@@ -33363,6 +33607,7 @@ async function localDoctor(runtime) {
|
|
|
33363
33607
|
origin: runtime.origin.origin,
|
|
33364
33608
|
credentialStorage: located === null ? "none" : located.store.backend.kind,
|
|
33365
33609
|
videoTools: await videoTools(),
|
|
33610
|
+
server: await probeServer(runtime.origin, timeoutMs),
|
|
33366
33611
|
diagnostic: "completed"
|
|
33367
33612
|
});
|
|
33368
33613
|
}
|
|
@@ -33375,7 +33620,7 @@ async function executeCommand(args, runtime) {
|
|
|
33375
33620
|
if (args.command === "help") {
|
|
33376
33621
|
return success2("help", "local", {
|
|
33377
33622
|
usage,
|
|
33378
|
-
commands: ["business", "capabilities", "credentials", "doctor", "eval", "harness", "login", "logout", "memory", "questions", "readiness", "research", "skill", "status", "strategies"]
|
|
33623
|
+
commands: ["business", "capabilities", "credentials", "doctor", "eval", "harness", "login", "logout", "memory", "questions", "readiness", "research", "skill", "status", "strategies", "support"]
|
|
33379
33624
|
});
|
|
33380
33625
|
}
|
|
33381
33626
|
if (args.command === "version") {
|
|
@@ -33384,6 +33629,13 @@ async function executeCommand(args, runtime) {
|
|
|
33384
33629
|
if (args.command === "capabilities") {
|
|
33385
33630
|
return success2("capabilities", "local", {
|
|
33386
33631
|
version: cliVersion,
|
|
33632
|
+
measurementMarkets: SUPPORTED_MARKETS,
|
|
33633
|
+
catalog: { version: CLI_DOLLAR_CATALOG_VERSION, currency: "usd", monthlyCents: {
|
|
33634
|
+
corePack: CLI_DOLLAR_PRICES.core,
|
|
33635
|
+
claudePerPack: CLI_DOLLAR_PRICES.claude,
|
|
33636
|
+
perplexityPerPack: CLI_DOLLAR_PRICES.perplexity
|
|
33637
|
+
} },
|
|
33638
|
+
support: { email: SUPPORT_EMAIL },
|
|
33387
33639
|
realLogin: "available",
|
|
33388
33640
|
paidExecution: "server_authorized",
|
|
33389
33641
|
expertise: { manifestVersion: AI_SEARCH_COMPETENCY_VERSION, competencies: AI_SEARCH_COMPETENCIES },
|
|
@@ -33402,13 +33654,14 @@ async function executeCommand(args, runtime) {
|
|
|
33402
33654
|
skill: { actions: ["path", "install"], options: ["--json"] },
|
|
33403
33655
|
status: { options: ["--json", "--eval-case", "--test-server", "--timeout-ms"] },
|
|
33404
33656
|
readiness: { options: ["--json", "--eval-case", "--timeout-ms"] },
|
|
33405
|
-
strategies: { options: ["--json", "--eval-case", "--timeout-ms"] }
|
|
33657
|
+
strategies: { options: ["--json", "--eval-case", "--timeout-ms"] },
|
|
33658
|
+
support: { tags: [...SUPPORT_TAGS], options: ["--json", "--tag", "--timeout-ms"] }
|
|
33406
33659
|
}
|
|
33407
33660
|
});
|
|
33408
33661
|
}
|
|
33409
33662
|
if (args.command === "doctor") {
|
|
33410
33663
|
if (args.testServer === void 0) {
|
|
33411
|
-
return localDoctor(runtime);
|
|
33664
|
+
return localDoctor(runtime, args.timeoutMs);
|
|
33412
33665
|
}
|
|
33413
33666
|
const status = await fetchTestStatus(args.testServer, args.timeoutMs);
|
|
33414
33667
|
return success2("doctor", "test", {
|
|
@@ -33418,7 +33671,7 @@ async function executeCommand(args, runtime) {
|
|
|
33418
33671
|
});
|
|
33419
33672
|
}
|
|
33420
33673
|
if (args.command === "skill") {
|
|
33421
|
-
const path =
|
|
33674
|
+
const path = fileURLToPath2(new URL("../skills/obi/SKILL.md", import.meta.url));
|
|
33422
33675
|
try {
|
|
33423
33676
|
await access(path);
|
|
33424
33677
|
} catch {
|
|
@@ -33431,7 +33684,7 @@ async function executeCommand(args, runtime) {
|
|
|
33431
33684
|
mode: "local"
|
|
33432
33685
|
});
|
|
33433
33686
|
}
|
|
33434
|
-
return success2("skill", "local", args.skillAction === "install" ? { action: "install", ...await installSkill(
|
|
33687
|
+
return success2("skill", "local", args.skillAction === "install" ? { action: "install", ...await installSkill(fileURLToPath2(new URL("../skills/obi", import.meta.url))) } : { action: "path", path });
|
|
33435
33688
|
}
|
|
33436
33689
|
if (args.command === "login") {
|
|
33437
33690
|
return success2("login", "product", await login(session, {
|
|
@@ -33488,6 +33741,9 @@ async function executeCommand(args, runtime) {
|
|
|
33488
33741
|
}
|
|
33489
33742
|
) : 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
33743
|
}
|
|
33744
|
+
if (args.command === "support") {
|
|
33745
|
+
return success2("support", "product", await sendSupport(session, { message: args.supportMessage, tag: args.supportTag ?? null }));
|
|
33746
|
+
}
|
|
33491
33747
|
if (args.command === "memory") {
|
|
33492
33748
|
if (args.memoryAction === "list") return success2("memory", "product", await listMemory(session));
|
|
33493
33749
|
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 +33796,51 @@ async function executeCommand(args, runtime) {
|
|
|
33540
33796
|
return success2("status", "product", await readStatus(session));
|
|
33541
33797
|
}
|
|
33542
33798
|
|
|
33799
|
+
// src/start.ts
|
|
33800
|
+
import { join as join7 } from "node:path";
|
|
33801
|
+
var linkStates = {
|
|
33802
|
+
created: "created",
|
|
33803
|
+
present: "present",
|
|
33804
|
+
off: "off (OBI_SKILL_LINKS), run obi skill install"
|
|
33805
|
+
};
|
|
33806
|
+
function renderStart(input) {
|
|
33807
|
+
const links = input.registration.state === "failed" ? `failed. ${sanitizeHumanText(input.registration.reason)}` : `${linkStates[input.registration.state]}.`;
|
|
33808
|
+
const account2 = input.accountEmail === null ? [
|
|
33809
|
+
"Account: not connected. Connect: obi login --expect-email <the user's email> (prints an address and a code for the user).",
|
|
33810
|
+
"Then: obi status --json"
|
|
33811
|
+
] : [`Account: connected as ${sanitizeHumanText(input.accountEmail)}. Its state: obi status --json`];
|
|
33812
|
+
return [
|
|
33813
|
+
`Obi CLI ${cliVersion}, npm package @hireobi/cli. Skill links: ${links}`,
|
|
33814
|
+
`Agent guide, a local file in this package: ${sanitizeHumanText(join7(input.skillDirectory, "SKILL.md"))}`,
|
|
33815
|
+
...account2,
|
|
33816
|
+
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.",
|
|
33817
|
+
'The obi command comes only from npm i -g @hireobi/cli; the npm package named "obi" is unrelated.'
|
|
33818
|
+
].join("\n") + "\n";
|
|
33819
|
+
}
|
|
33820
|
+
|
|
33543
33821
|
// src/welcome.ts
|
|
33544
33822
|
var copy = {
|
|
33545
33823
|
en: {
|
|
33546
33824
|
tagline: "Your AI Search employee.",
|
|
33547
33825
|
greeting: "Hi, I'm Obi. Let's get you discovered.",
|
|
33548
|
-
|
|
33826
|
+
ready: "I'm in your assistants: Claude Code, Codex, Cursor and OpenCode.",
|
|
33827
|
+
off: "Get started: obi skill install",
|
|
33828
|
+
failed: "I could not join your assistants.",
|
|
33549
33829
|
claude: "Claude Code / Cursor: /obi",
|
|
33550
33830
|
codex: "Codex: $obi or the skills picker",
|
|
33831
|
+
opencode: "OpenCode: @obi",
|
|
33551
33832
|
next: "Then give me your website in your assistant's chat.",
|
|
33552
33833
|
help: "All commands: obi --help"
|
|
33553
33834
|
},
|
|
33554
33835
|
fr: {
|
|
33555
33836
|
tagline: "Ton agent AI Search.",
|
|
33556
33837
|
greeting: "Salut, moi c'est Obi. Faisons conna\xEEtre ton entreprise.",
|
|
33557
|
-
|
|
33838
|
+
ready: "Je suis dans tes assistants : Claude Code, Codex, Cursor et OpenCode.",
|
|
33839
|
+
off: "Pour d\xE9marrer : obi skill install",
|
|
33840
|
+
failed: "Je n'ai pas pu rejoindre tes assistants.",
|
|
33558
33841
|
claude: "Claude Code / Cursor : /obi",
|
|
33559
33842
|
codex: "Codex : $obi ou le s\xE9lecteur de skills",
|
|
33843
|
+
opencode: "OpenCode : @obi",
|
|
33560
33844
|
next: "Donne-moi ensuite ton site dans ton assistant.",
|
|
33561
33845
|
help: "Toutes les commandes : obi --help"
|
|
33562
33846
|
}
|
|
@@ -33610,7 +33894,7 @@ function mascotLine(row, depth) {
|
|
|
33610
33894
|
return `${color(top, false, depth)}${color(bottom, true, depth)}\u2580`;
|
|
33611
33895
|
}).join("") + reset;
|
|
33612
33896
|
}
|
|
33613
|
-
function renderWelcome(env, columns, colorDepth) {
|
|
33897
|
+
function renderWelcome(env, columns, colorDepth, registration) {
|
|
33614
33898
|
const locale = [env.OBI_LANGUAGE, env.LC_ALL, env.LC_MESSAGES, env.LANG].find((value) => value?.trim())?.trim().toLowerCase() ?? "en";
|
|
33615
33899
|
const text2 = copy[/^fr(?:[-_.@]|$)/.test(locale) ? "fr" : "en"];
|
|
33616
33900
|
const colorful = columns >= 64 && colorDepth >= 8 && env.TERM !== "dumb" && !env.NO_COLOR;
|
|
@@ -33619,14 +33903,16 @@ function renderWelcome(env, columns, colorDepth) {
|
|
|
33619
33903
|
return ` ${mascotLine(row, colorDepth)} ${aside}`;
|
|
33620
33904
|
}).join("\n") : `Obi \xB7 ${cliVersion}
|
|
33621
33905
|
${text2.tagline}`;
|
|
33906
|
+
const skill = registration.state === "failed" ? `${text2.failed} ${sanitizeHumanText(registration.reason)}` : registration.state === "off" ? text2.off : text2.ready;
|
|
33622
33907
|
return `
|
|
33623
33908
|
${title}
|
|
33624
33909
|
|
|
33625
33910
|
${text2.greeting}
|
|
33626
33911
|
|
|
33627
|
-
${
|
|
33912
|
+
${skill}
|
|
33628
33913
|
${text2.claude}
|
|
33629
33914
|
${text2.codex}
|
|
33915
|
+
${text2.opencode}
|
|
33630
33916
|
|
|
33631
33917
|
${text2.next}
|
|
33632
33918
|
${text2.help}
|
|
@@ -33648,7 +33934,8 @@ function failureResult(error51, command, mode) {
|
|
|
33648
33934
|
nextAction: error51.nextAction
|
|
33649
33935
|
},
|
|
33650
33936
|
authorization: error51.authorization,
|
|
33651
|
-
account: error51.account
|
|
33937
|
+
account: error51.account,
|
|
33938
|
+
support: error51.support
|
|
33652
33939
|
});
|
|
33653
33940
|
}
|
|
33654
33941
|
function markOrigin(result, origin) {
|
|
@@ -33658,6 +33945,8 @@ async function runCli() {
|
|
|
33658
33945
|
const argv = process.argv.slice(2);
|
|
33659
33946
|
const json2 = wantsJson(argv);
|
|
33660
33947
|
const welcome = argv.length === 0 && process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
33948
|
+
const start = argv.length === 0 && !welcome;
|
|
33949
|
+
const registration = await registerSkillLinks(packagedSkillDirectory(), process.env);
|
|
33661
33950
|
let command = "unknown";
|
|
33662
33951
|
let mode = commandMode(
|
|
33663
33952
|
void 0,
|
|
@@ -33675,14 +33964,21 @@ async function runCli() {
|
|
|
33675
33964
|
return resolved;
|
|
33676
33965
|
};
|
|
33677
33966
|
try {
|
|
33678
|
-
const args = parseCommandArguments(welcome ? ["--help"] : argv);
|
|
33967
|
+
const args = parseCommandArguments(welcome || start ? ["--help"] : argv);
|
|
33679
33968
|
command = args.command;
|
|
33680
33969
|
mode = commandMode(args.command, args.testServer !== void 0);
|
|
33681
33970
|
origin = activeOrigin();
|
|
33682
|
-
const
|
|
33683
|
-
|
|
33684
|
-
|
|
33685
|
-
|
|
33971
|
+
const runtime = { origin, stateDirectory: await prepareStateDirectory(process.env) };
|
|
33972
|
+
if (start) {
|
|
33973
|
+
const located = await locateCredential(runtime, "skip");
|
|
33974
|
+
process.stdout.write(renderStart({
|
|
33975
|
+
registration,
|
|
33976
|
+
skillDirectory: packagedSkillDirectory(),
|
|
33977
|
+
accountEmail: located === null ? null : located.credential.account.email
|
|
33978
|
+
}));
|
|
33979
|
+
return;
|
|
33980
|
+
}
|
|
33981
|
+
const result = await executeCommand(args, runtime);
|
|
33686
33982
|
const evaluationCaseId = args.evaluationCaseId;
|
|
33687
33983
|
const scopeNext = (data) => ({
|
|
33688
33984
|
...data,
|
|
@@ -33694,7 +33990,7 @@ async function runCli() {
|
|
|
33694
33990
|
...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
33991
|
}) : result;
|
|
33696
33992
|
if (scoped.ok && welcome) {
|
|
33697
|
-
process.stdout.write(renderWelcome(process.env, process.stdout.columns, process.stdout.getColorDepth()));
|
|
33993
|
+
process.stdout.write(renderWelcome(process.env, process.stdout.columns, process.stdout.getColorDepth(), registration));
|
|
33698
33994
|
} else {
|
|
33699
33995
|
writeResult(markOrigin(scoped, origin), args.json);
|
|
33700
33996
|
}
|
|
@@ -33712,7 +34008,14 @@ async function runCli() {
|
|
|
33712
34008
|
retryable: false,
|
|
33713
34009
|
nextAction: "Run the command again or inspect the local CLI installation."
|
|
33714
34010
|
});
|
|
33715
|
-
|
|
34011
|
+
const failure2 = failureResult(knownError, knownError.command ?? command, mode);
|
|
34012
|
+
if (failure2.mode !== "test" && failure2.command !== "support" && knownError.code !== "AUTHORIZATION_PENDING") {
|
|
34013
|
+
await prepareStateDirectory(process.env).then((directory) => recordLastError(
|
|
34014
|
+
directory,
|
|
34015
|
+
{ code: knownError.code, command: failure2.command }
|
|
34016
|
+
), () => void 0);
|
|
34017
|
+
}
|
|
34018
|
+
writeResult(markOrigin(failure2, origin), json2);
|
|
33716
34019
|
process.exitCode = knownError.exitCode;
|
|
33717
34020
|
} finally {
|
|
33718
34021
|
closeIdleConnections();
|
package/package.json
CHANGED
package/skills/obi/SKILL.md
CHANGED
|
@@ -33,7 +33,7 @@ Obi remembers what the customer tells it to remember, across hosts and sessions:
|
|
|
33
33
|
|
|
34
34
|
Check `obi capabilities --json`, then `obi memory list --json`. Keep the account context established by the conversation or CLI result. For the ordinary connected business, use `obi status --json` and work commands without `--eval-case`; do not look for an evaluation ID. Only an explicitly established public-company evaluation uses `--eval-case <id>` on every account or work read and write, including `status`. `obi eval list --json` recovers a missing ID for that already selected evaluation; a missing ID alone never establishes one. If the machine is unconnected, follow [connection](references/connection.md). If the customer names a public company to evaluate, use its domain only after they choose it inside this conversation. An eligible operator starts an isolated case with `obi eval start <domain> --json`. Never use the ordinary account's old results for a new domain.
|
|
35
35
|
|
|
36
|
-
When no domain is chosen, offer to continue the connected company's saved work or evaluate a new public domain. Ask once for the research mission: the domain, intended buyer or market if known, and whether you may read its public pages and relevant public competitors. Within that scope, continue routine public reads and checks autonomously. Read [adaptive research](references/host-work.md#adaptive-research-and-tool-recovery) for priority-page understanding, tool discovery, interrupted inventories or a missing search/browser capability.
|
|
36
|
+
When no domain is chosen, offer to continue the connected company's saved work or evaluate a new public domain. Ask once for the research mission: the domain, intended buyer or market if known, and whether you may read its public pages and relevant public competitors. Before any work is spent, run the [fit check](references/fit-and-scope.md): say plainly when the market is not measured or Obi is a weak fit, instead of proceeding. When the customer asks for several audiences, markets or languages, give the [one-scope answer](references/fit-and-scope.md#several-audiences-markets-or-languages). Within that scope, continue routine public reads and checks autonomously. Read [adaptive research](references/host-work.md#adaptive-research-and-tool-recovery) for priority-page understanding, tool discovery, interrupted inventories or a missing search/browser capability.
|
|
37
37
|
|
|
38
38
|
Read `obi harness coverage --json` for the selected case before new Business work. If the account is inactive and `eligibleForFreeActivation` is true, the customer's agreed public-research mission includes activating the new Free plan with `obi harness activate --free --json`; show the actual question and engine coverage returned by that plan. An existing active plan needs no activation. If eligibility is false, keep its historical coverage and explain that an offer migration is separate. Activation does not start a scan. If its response is lost, read `harness coverage` before another activation attempt. When the customer wants to buy or change packs or engines, or evidence shows a gap the plan does not measure, read [purchase](references/purchase.md).
|
|
39
39
|
|
|
@@ -51,6 +51,8 @@ When the customer reports an implemented action, read [outcome observations](ref
|
|
|
51
51
|
|
|
52
52
|
Lead with the outcome, material uncertainty and one useful next action. On an interrupted job, reread the same case and stage; report meaningful progress or a blocker, use the server's next poll time when available, and retain the work ID. A timeout on a write calls for a read before any retry. Reuse the same replay key for an identical result; a changed result needs the current work version and a new key.
|
|
53
53
|
|
|
54
|
+
When a failure cannot be recovered in this conversation (the same error after the retry its `nextAction` names, or `obi doctor --json` reporting `server` as `degraded` or `unreachable`), say so and offer once to send it to Obi support. On the customer's yes, run `obi support "<what happened, in their words>" --json`: it adds the CLI version, the system and the last error code itself. When it fails with `SUPPORT_NOT_SENT`, nothing was sent: give them support@hireobi.com and the `support.diagnosticText` to paste. Never put a token or a credential file in the message.
|
|
55
|
+
|
|
54
56
|
Separate site observations, customer confirmations, host inferences and independently measured answers. Request only the private extracts needed for the agreed scope; keep secrets and unrelated workspace material local. A local draft or patch is a reviewable suggestion until the customer accepts it. The Skill runs when invoked; while the harness is closed, saved server measurements can continue but host research and reasoning wait for resumption.
|
|
55
57
|
|
|
56
58
|
Stay in customer mode even inside the Obi source repository. A product-use failure is explained and recovered in this conversation. Open a development task, branch or pull request only after an explicit separate software-development request.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Fit, markets and scope
|
|
2
|
+
|
|
3
|
+
Read before the first research of a business, and when the customer asks for several audiences, markets or languages. Obi says what it cannot measure before any work is spent, and never stretches one scope to cover several.
|
|
4
|
+
|
|
5
|
+
## Fit check, before any work
|
|
6
|
+
|
|
7
|
+
Judge the fit from the domain, the customer's words and one read of the home page, before activating a plan or starting research. `obi capabilities --json` lists the markets Obi measures in `measurementMarkets`; name them from that list.
|
|
8
|
+
|
|
9
|
+
| What you see | What you say | What you do next |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| The buyers are in a market outside `measurementMarkets` | "Obi does not measure <market> yet. It measures in: <the names from `measurementMarkets`>." | Ask whether they also sell in a listed market. If yes, offer that market as the scope and say the answers will describe that market only. If no, stop there. Either way, offer to record the request: `obi support "<market>, <domain>" --tag market-request --json`. |
|
|
12
|
+
| The buyers search in a language no listed market uses | The same sentence, naming the language and the markets. | The same two offers. |
|
|
13
|
+
| No AI-search buying journey: buyers choose without asking an assistant (an impulse or habit purchase, a walk-in local trade, a sale won only by tender or by marketplace listing) | "Obi is a weak fit for <business>: <the reason from what you read>. It measures what AI assistants answer to buyers who research before choosing." | Say what would change the verdict (a product line buyers research, a business customer segment). Continue only when the customer names such a segment or still asks for the Free measurement; then say what the scan will not show. |
|
|
14
|
+
| The company has no public pages to read, or blocks them | "Obi reads public pages; it cannot ground questions without them." | Ask for the public domain that carries the offer; without one, stop. |
|
|
15
|
+
|
|
16
|
+
`obi support` sends only from a signed-in machine. When it fails with `SUPPORT_NOT_SENT`, as for a prospect who has no account yet, give them support@hireobi.com and the `support.diagnosticText` to paste.
|
|
17
|
+
|
|
18
|
+
A weak fit is a judgment you state with its evidence, as [business judgment](business-judgment.md) describes; the customer may correct the fact it rests on. Say it once, in one or two sentences, then the one next action. A supported market with a clear buying journey needs no fit statement.
|
|
19
|
+
|
|
20
|
+
When the server answers `MARKET_UNSUPPORTED` to a questions submission, the same rule applies: say it plainly, name the markets, offer the request.
|
|
21
|
+
|
|
22
|
+
## Several audiences, markets or languages
|
|
23
|
+
|
|
24
|
+
One scope is one market, one primary audience and one measured language: the language buyers of that market use. An account holds one scope; packs add questions inside it. There is no second scope on an account today, so it has no price.
|
|
25
|
+
|
|
26
|
+
When the customer asks for more (for example three ICPs, or three languages), answer in this order:
|
|
27
|
+
|
|
28
|
+
1. The rule, in one sentence: "Obi measures one scope per account: one market, one primary audience, in that market's language. More packs add questions for that scope; they do not add a second one."
|
|
29
|
+
2. Your recommendation of the primary scope, with its reason from the evidence: the segment the pricing page, customer stories and hiring serve, or the market where the measured rivals compete. Recommend one; ask only for confirmation.
|
|
30
|
+
3. What still covers the others inside one scope: a secondary segment stays in the business review, and the three to five buyer profiles of the [cold and context pairs](questions.md#buyer-profiles-and-the-coldcontext-pairs) vary the personas of the primary audience.
|
|
31
|
+
4. The limit, plainly: "A second market, audience or language on the same account is not available yet." Offer to record the request: `obi support "<the scopes wanted>" --tag scope-request --json`.
|
|
32
|
+
5. Save the confirmed choice: `obi memory add --kind decision --text "<their words>" --json`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Buying paid packs
|
|
2
2
|
|
|
3
|
-
Read when the customer wants to buy packs or change the packs or engines of a paid plan, and before proposing a change yourself. Buying happens in this conversation, where the customer reviews the questions each pack will measure; the `/billing` page shows the plan and keeps cards, invoices and cancellation. Every
|
|
3
|
+
Read when the customer wants to buy packs or change the packs or engines of a paid plan, and before proposing a change yourself. Buying happens in this conversation, where the customer reviews the questions each pack will measure; the `/billing` page shows the plan and keeps cards, invoices and cancellation. Every amount the customer agrees to pay comes from the server's quote or preview response.
|
|
4
4
|
|
|
5
5
|
Only the owner of the connected business account buys, with a verified email; a public-company evaluation case cannot.
|
|
6
6
|
|
|
@@ -70,7 +70,38 @@ A change that adds and removes at once (`PLAN_CHANGE_TWO_STEPS`) runs in two ste
|
|
|
70
70
|
|
|
71
71
|
## Proposing a change yourself
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
Propose a change only when a measurement shows a gap the plan does not cover. Each trigger needs its evidence from the saved results (`obi questions results --json`, `obi harness evidence`), named by question and date:
|
|
74
|
+
|
|
75
|
+
| Trigger | Measured evidence it needs | What you propose |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| Perplexity | On research-style buying questions (comparisons, alternatives, "which should we use"), the scan's answers name rivals with cited sources while the company is absent or uncited. | Perplexity on those questions, 1 to 10 per pack. |
|
|
78
|
+
| Claude | The confirmed primary audience is technical or B2B, and on the same measured buying questions the engines already disagree on the brands they name. Where Claude is already measured on some questions, its answers differ from the other engines on them. | Claude on those buying questions, 1 to 10 per pack. |
|
|
79
|
+
| More packs | Buying situations in the evidence (a confirmed segment's situations, a rival's angle, the sources the answers cite) that no reviewed question covers, while every current question covers a distinct one. | One more pack, with the situations it would add. |
|
|
80
|
+
| Paid from Free | Two monthly scans differ (a rival enters or leaves the answers, the company's presence changes) and nothing dates the change inside the month, or the customer reported an action whose effect the next monthly scan would show weeks late. | The first pack, for its weekly scan. |
|
|
81
|
+
|
|
82
|
+
The host's own answer to a question is never this evidence, and an engine the plan does not measure has no measured answers to cite.
|
|
83
|
+
|
|
84
|
+
A proposal is one sentence of evidence, the price, one question. Cite the server's price whenever a quote is possible; both commands below charge nothing:
|
|
85
|
+
|
|
86
|
+
| Plan and change | Price to cite |
|
|
87
|
+
|---|---|
|
|
88
|
+
| A paid plan, adding an engine on questions it already measures | Write the quote file of the new plan and run `obi harness purchase change --input <file> --json` ([changing a paid plan](#changing-a-paid-plan), step 2): cite `amountDueCents` now and the new `quote.monthlyAmountCents`. |
|
|
89
|
+
| No paid plan, with a reviewed set of 25 questions per pack | `obi harness purchase quote --input <file> --json`: cite `monthlyAmountCents`. |
|
|
90
|
+
| The change needs questions not reviewed yet (one more pack, a first pack without a reviewed set) | No quote is possible yet: cite the list price below, labelled "list price", and offer to prepare the questions and the quote. |
|
|
91
|
+
|
|
92
|
+
The list price is what the change adds each month, computed from `obi capabilities --json` (`catalog.monthlyCents`) and the plan in `obi harness coverage --json`:
|
|
93
|
+
|
|
94
|
+
| Change | Monthly amount it adds |
|
|
95
|
+
|---|---|
|
|
96
|
+
| An engine on a paid plan | Its per-pack amount times the packs of the plan (`plan.questionCount` divided by 25). |
|
|
97
|
+
| One more pack | `corePack`, plus the per-pack amount of each engine already on the plan (an engine whose `plan.engineQuestionCounts` is above zero). |
|
|
98
|
+
| The first pack from Free | `corePack`. |
|
|
99
|
+
|
|
100
|
+
A list price is before any discount on the account; the quote or preview confirms the exact amount before any yes to pay. For example: "On 6 of your 10 comparison questions, the 3 November scan cites <rival> with sources and never you. Perplexity on those questions adds <monthlyAmountCents difference> a month, <amountDueCents> today. Shall I apply it?"
|
|
101
|
+
|
|
102
|
+
- Propose once per gap, and at most one proposal in a reply. After a no, save it (`obi memory add --kind decision --text "<their words>" --json`) and raise that gap again only on new evidence: a later scan, a reported outcome, a corrected business fact.
|
|
103
|
+
- Optional engines are never filled to quota: propose the buying questions the evidence names, however few.
|
|
104
|
+
- No proposal while a failure is unresolved, before the first results exist, or as the answer to a question the customer asked about something else.
|
|
74
105
|
|
|
75
106
|
## What waits for the customer
|
|
76
107
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Buyer questions, scan and priorities
|
|
2
2
|
|
|
3
|
-
After the business review is confirmed, inspect `obi harness show --stage questions --json`. Show the supported measurement markets and confirmed buyer audiences from its work context. Ask the customer to choose one market and one primary audience; a suggested market is guidance, not consent or a default. Prepare the displayed coverage count in the host and save it with the versioned `harness submit --stage questions --input <file> --json` path described in [host work](host-work.md). `obi questions show --json` reads the accepted set and its preparation state. A queued or running state means wait and inspect again after meaningful progress; do not announce that questions already exist.
|
|
3
|
+
After the business review is confirmed, inspect `obi harness show --stage questions --json`. Show the supported measurement markets and confirmed buyer audiences from its work context. Ask the customer to choose one market and one primary audience; a suggested market is guidance, not consent or a default. When they ask for several, give the [one-scope answer](fit-and-scope.md#several-audiences-markets-or-languages). Prepare the displayed coverage count in the host and save it with the versioned `harness submit --stage questions --input <file> --json` path described in [host work](host-work.md). `obi questions show --json` reads the accepted set and its preparation state. A queued or running state means wait and inspect again after meaningful progress; do not announce that questions already exist.
|
|
4
4
|
|
|
5
5
|
## Writing the questions
|
|
6
6
|
|