oomi-ai 0.8.3 → 0.8.5
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 +9 -9
- package/agent_instructions.md +9 -9
- package/bin/oomi-ai.js +31 -11
- package/lib/connectionSetup.js +2 -2
- package/lib/grokbotRuntime.js +158 -17
- package/lib/processSupervisor.js +137 -0
- package/package.json +5 -3
- package/skills/oomi/SKILL.md +9 -9
- package/skills/oomi/agent_instructions.md +9 -9
- package/skills/oomi-delivery/SKILL.md +18 -0
package/README.md
CHANGED
|
@@ -48,32 +48,32 @@ This package exposes connection, bridge, voice, and persona app state APIs. It d
|
|
|
48
48
|
|
|
49
49
|
Connect the agent the user is speaking to. Identify your runtime, bot/profile, and actual host first. Computer access does not mean you run on that computer. Never substitute another installed agent.
|
|
50
50
|
|
|
51
|
-
## Choose a connection by interface (oomi-ai 0.8.
|
|
51
|
+
## Choose a connection by interface (oomi-ai 0.8.5+)
|
|
52
52
|
|
|
53
53
|
Run setup on the host where **this agent** runs. A Grok Bot with access to someone's Mac still runs on its Grok Bot host; connecting an OpenClaw agent on that Mac does not connect the Grok Bot. No new model API key or replacement bot is needed.
|
|
54
54
|
|
|
55
55
|
1. Identify yourself from your current runtime context: bot/profile identity, host, and conversation behavior. Do not guess from installed executables or names.
|
|
56
|
-
2. Run `oomi capabilities`. OpenClaw and Hermes have native adapters. Grok Bot
|
|
56
|
+
2. Run `oomi capabilities`. OpenClaw and Hermes have native adapters. Grok Bot uses its own local gateway through the adapter described below. An unlisted brand is not automatically unsupported: inspect its documented message input **and completed-reply** interfaces (CLI, API, SDK, webhook, or inbound channel).
|
|
57
57
|
3. For another runtime, use `--runtime command` to wrap those interfaces and return `{text, sessionId?}` only when the same agent has finished. A health endpoint or an accepted/queued response is not a final reply. If no supported input or reply interface exists, state that specific missing capability; never invent one or select another agent.
|
|
58
58
|
4. Preserve identity, memories and host approvals. Say whether you resume an exact existing conversation or keep an Oomi conversation with the same agent. Verify a real reply before sharing an Auth Link; verify delivery in Oomi before claiming completion.
|
|
59
59
|
|
|
60
60
|
### Grok Bot: connect the existing bot
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
On the Grok Bot host, run `oomi grokbot instructions --name YOUR_CONNECTION --agent-id YOUR_BOT_UUID`. Follow the printed fixed routine instruction with the bot's normal routine tools. Save the platform-issued HTTPS webhook URL and key to the printed private `grokbot.json` (mode 0600). Never paste credentials into chat. Then run:
|
|
62
|
+
On **this bot's own Linux host**, install/update the package through the normal host approval flow, then run:
|
|
65
63
|
|
|
66
64
|
```bash
|
|
67
65
|
oomi connect --runtime grokbot --name YOUR_CONNECTION --agent-id YOUR_BOT_UUID --agent-name "Your name" --app-url https://www.oomi.ai
|
|
68
66
|
```
|
|
69
67
|
|
|
70
|
-
|
|
68
|
+
Use your exact bot UUID from your runtime context. Never choose whichever bot happens to be active on a shared computer. Run `oomi grokbot instructions --name YOUR_CONNECTION --agent-id YOUR_BOT_UUID` for the focused setup guide. The package uses the host's existing private local gateway; **do not ask the user to copy a webhook URL/key or create a routine**. Fresh connections require no `grokbot.json`. Existing explicit webhook configurations remain supported. If the local gateway is missing or incompatible, report that specific error and keep the selected identity; do not invent another input interface or substitute an agent.
|
|
69
|
+
|
|
70
|
+
Setup returns **queued**, then waits in the background. Finish your current bot turn immediately so the verification can execute; waiting for your own verification in that turn deadlocks the host. The package installs its private Oomi delivery skill and waits until this bot has loaded it. Incoming chat shows the original message with a small skill tag; the skill claims the private request and submits its final reply. Missing or locally modified skills fail before sending; do not replace them with instruction walls in chat. Continue only for `status: claimed`. A verification answer must be exactly `OOMI_OK`. The reply command reports one Auth Link only after verification, pairing and bridge subscription succeed. Show the Auth Link to the user. If pending, finish the turn and use `oomi grokbot status --name YOUR_CONNECTION` later. Queued is not connected. Acceptance by the host is not a completed answer.
|
|
71
71
|
|
|
72
|
-
Use
|
|
72
|
+
Use explicit `--connection YOUR_CONNECTION` for Oomi account commands: a shared host does not inherit the bridge's environment. Text and voice transcripts are supported; photos are rejected. Oomi supplies conversation history; exact Grok Bot desktop/DM conversation continuation is not guaranteed. Host approvals remain in Grok Bot. Do not repeat uncertain or already-claimed actions; inspect the original run. Reconnect reuses verification and pairing; `oomi verify --name YOUR_CONNECTION` explicitly queues a new model verification.
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
Oomi uses systemd user services where available. On Linux hosts without systemd, it automatically supervises bridge worker exits for the current host session. **After the whole cloud computer restarts, rerun the same connect command.** This fallback cannot start itself at boot. Use `oomi status --name YOUR_CONNECTION --json` to inspect recovery. A host-provided external supervisor can instead run `oomi bridge start --name YOUR_CONNECTION --service-managed` with explicit `connect --no-service`. Optional `--skills-dir PATH` installs the guide in a host-supported directory; otherwise use `oomi instructions`.
|
|
75
75
|
|
|
76
|
-
The adapter
|
|
76
|
+
The direct adapter targets the installed Grok Bot local gateway contract. An incompatible provider update fails closed. Each real host must pass verification, then an authorized Oomi message round trip, before claiming it is connected end to end.
|
|
77
77
|
|
|
78
78
|
## Updating Oomi safely
|
|
79
79
|
|
package/agent_instructions.md
CHANGED
|
@@ -2,32 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Connect the agent the user is speaking to. Identify your runtime, bot/profile, and actual host first. Computer access does not mean you run on that computer. Never substitute another installed agent.
|
|
4
4
|
|
|
5
|
-
## Choose a connection by interface (oomi-ai 0.8.
|
|
5
|
+
## Choose a connection by interface (oomi-ai 0.8.5+)
|
|
6
6
|
|
|
7
7
|
Run setup on the host where **this agent** runs. A Grok Bot with access to someone's Mac still runs on its Grok Bot host; connecting an OpenClaw agent on that Mac does not connect the Grok Bot. No new model API key or replacement bot is needed.
|
|
8
8
|
|
|
9
9
|
1. Identify yourself from your current runtime context: bot/profile identity, host, and conversation behavior. Do not guess from installed executables or names.
|
|
10
|
-
2. Run `oomi capabilities`. OpenClaw and Hermes have native adapters. Grok Bot
|
|
10
|
+
2. Run `oomi capabilities`. OpenClaw and Hermes have native adapters. Grok Bot uses its own local gateway through the adapter described below. An unlisted brand is not automatically unsupported: inspect its documented message input **and completed-reply** interfaces (CLI, API, SDK, webhook, or inbound channel).
|
|
11
11
|
3. For another runtime, use `--runtime command` to wrap those interfaces and return `{text, sessionId?}` only when the same agent has finished. A health endpoint or an accepted/queued response is not a final reply. If no supported input or reply interface exists, state that specific missing capability; never invent one or select another agent.
|
|
12
12
|
4. Preserve identity, memories and host approvals. Say whether you resume an exact existing conversation or keep an Oomi conversation with the same agent. Verify a real reply before sharing an Auth Link; verify delivery in Oomi before claiming completion.
|
|
13
13
|
|
|
14
14
|
### Grok Bot: connect the existing bot
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
On the Grok Bot host, run `oomi grokbot instructions --name YOUR_CONNECTION --agent-id YOUR_BOT_UUID`. Follow the printed fixed routine instruction with the bot's normal routine tools. Save the platform-issued HTTPS webhook URL and key to the printed private `grokbot.json` (mode 0600). Never paste credentials into chat. Then run:
|
|
16
|
+
On **this bot's own Linux host**, install/update the package through the normal host approval flow, then run:
|
|
19
17
|
|
|
20
18
|
```bash
|
|
21
19
|
oomi connect --runtime grokbot --name YOUR_CONNECTION --agent-id YOUR_BOT_UUID --agent-name "Your name" --app-url https://www.oomi.ai
|
|
22
20
|
```
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
Use your exact bot UUID from your runtime context. Never choose whichever bot happens to be active on a shared computer. Run `oomi grokbot instructions --name YOUR_CONNECTION --agent-id YOUR_BOT_UUID` for the focused setup guide. The package uses the host's existing private local gateway; **do not ask the user to copy a webhook URL/key or create a routine**. Fresh connections require no `grokbot.json`. Existing explicit webhook configurations remain supported. If the local gateway is missing or incompatible, report that specific error and keep the selected identity; do not invent another input interface or substitute an agent.
|
|
23
|
+
|
|
24
|
+
Setup returns **queued**, then waits in the background. Finish your current bot turn immediately so the verification can execute; waiting for your own verification in that turn deadlocks the host. The package installs its private Oomi delivery skill and waits until this bot has loaded it. Incoming chat shows the original message with a small skill tag; the skill claims the private request and submits its final reply. Missing or locally modified skills fail before sending; do not replace them with instruction walls in chat. Continue only for `status: claimed`. A verification answer must be exactly `OOMI_OK`. The reply command reports one Auth Link only after verification, pairing and bridge subscription succeed. Show the Auth Link to the user. If pending, finish the turn and use `oomi grokbot status --name YOUR_CONNECTION` later. Queued is not connected. Acceptance by the host is not a completed answer.
|
|
25
25
|
|
|
26
|
-
Use
|
|
26
|
+
Use explicit `--connection YOUR_CONNECTION` for Oomi account commands: a shared host does not inherit the bridge's environment. Text and voice transcripts are supported; photos are rejected. Oomi supplies conversation history; exact Grok Bot desktop/DM conversation continuation is not guaranteed. Host approvals remain in Grok Bot. Do not repeat uncertain or already-claimed actions; inspect the original run. Reconnect reuses verification and pairing; `oomi verify --name YOUR_CONNECTION` explicitly queues a new model verification.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Oomi uses systemd user services where available. On Linux hosts without systemd, it automatically supervises bridge worker exits for the current host session. **After the whole cloud computer restarts, rerun the same connect command.** This fallback cannot start itself at boot. Use `oomi status --name YOUR_CONNECTION --json` to inspect recovery. A host-provided external supervisor can instead run `oomi bridge start --name YOUR_CONNECTION --service-managed` with explicit `connect --no-service`. Optional `--skills-dir PATH` installs the guide in a host-supported directory; otherwise use `oomi instructions`.
|
|
29
29
|
|
|
30
|
-
The adapter
|
|
30
|
+
The direct adapter targets the installed Grok Bot local gateway contract. An incompatible provider update fails closed. Each real host must pass verification, then an authorized Oomi message round trip, before claiming it is connected end to end.
|
|
31
31
|
|
|
32
32
|
## Discover and use Oomi (oomi-ai 0.7.0+)
|
|
33
33
|
|
package/bin/oomi-ai.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { createHostProcessService } from '../lib/processSupervisor.js';
|
|
3
|
+
import { grokbotConfig, grokbotInstructions, grokbotInboxRoots, receiveGrokbotRequest, claimGrokbotRequest, replyGrokbotRequest, waitForGrokbotIdle } from '../lib/grokbotRuntime.js';
|
|
3
4
|
import { queueConnectionSetup, readConnectionSetup, requireConnectionSetup, finishConnectionSetup, waitConnectionSetup } from '../lib/connectionSetup.js';
|
|
4
5
|
import fs from 'fs';
|
|
5
6
|
import os from 'os';
|
|
@@ -267,6 +268,7 @@ Commands:
|
|
|
267
268
|
Apply an approved persona app action through the Oomi backend.
|
|
268
269
|
|
|
269
270
|
grokbot instructions --name CONNECTION --agent-id UUID
|
|
271
|
+
connect --runtime grokbot --name CONNECTION --agent-id UUID [--grokbot-gateway /absolute/path/to/gateway.json]
|
|
270
272
|
grokbot status|claim|reply --name CONNECTION [--request ID --agent-id UUID --file PATH]
|
|
271
273
|
capabilities
|
|
272
274
|
Discover Spaces, health, routines, identity, permissions, and cost policy.
|
|
@@ -858,11 +860,17 @@ function resolveBridgeLiveLogPath() {
|
|
|
858
860
|
return bridgeConnectionName ? path.join(connectionDirectory(), 'logs', 'oomi-bridge-live.log') : resolveOpenclawBridgeLiveLogPath();
|
|
859
861
|
}
|
|
860
862
|
|
|
861
|
-
function currentBridgeService() {
|
|
862
|
-
|
|
863
|
+
function currentBridgeService(runtime = readBridgeState().runtime) {
|
|
864
|
+
const config = bridgeServiceConfig({
|
|
863
865
|
name: bridgeConnectionName, stateDirectory: path.dirname(resolveBridgeStatePath()),
|
|
864
866
|
logPath: resolveBridgeLiveLogPath(), scriptPath: fileURLToPath(import.meta.url),
|
|
865
|
-
})
|
|
867
|
+
});
|
|
868
|
+
const system = createBridgeService(config);
|
|
869
|
+
if (runtime === 'grokbot' && process.platform === 'linux') {
|
|
870
|
+
const host = createHostProcessService(config);
|
|
871
|
+
if (fs.existsSync(host.config.file) || (!fs.existsSync(config.file) && !system.availability().available)) return host;
|
|
872
|
+
}
|
|
873
|
+
return system;
|
|
866
874
|
}
|
|
867
875
|
|
|
868
876
|
function buildBridgeLaunchAgentPlist() {
|
|
@@ -4065,7 +4073,7 @@ function printOpenclawBridgeStatus(flags) {
|
|
|
4065
4073
|
if (payload.maintenance) console.log(`Maintenance: ${payload.maintenance.status} — ${payload.maintenance.message || ''}`);
|
|
4066
4074
|
console.log('Oomi Bridge Status');
|
|
4067
4075
|
const supervision = payload.supervision;
|
|
4068
|
-
console.log(`Automatic startup: ${supervision.installed && supervision.enabled ? supervision.manager + ' (' + supervision.startsAt + ')' : 'off'}`);
|
|
4076
|
+
console.log(supervision.manager === 'host-process' ? `Worker recovery: ${supervision.enabled ? 'on' : 'off'} for this host session; reconnect after a cloud computer restart.` : `Automatic startup: ${supervision.installed && supervision.enabled ? supervision.manager + ' (' + supervision.startsAt + ')' : 'off'}`);
|
|
4069
4077
|
if (!supervision.installed || !supervision.enabled || !supervision.running) console.log(`Repair on this computer: oomi bridge repair${bridgeConnectionName ? ' --name ' + bridgeConnectionName : ''}`);
|
|
4070
4078
|
if (supervision.reason) console.log(supervision.reason);
|
|
4071
4079
|
console.log('------------------');
|
|
@@ -4270,7 +4278,7 @@ async function repairBridge(flags = {}) {
|
|
|
4270
4278
|
await handleBridgeServiceCommand('install', flags);
|
|
4271
4279
|
if (!isTruthyFlag(flags['no-start'])) {
|
|
4272
4280
|
await waitForBridgeConnection({});
|
|
4273
|
-
console.log('Oomi bridge restored with automatic startup. Pairing and conversations preserved.');
|
|
4281
|
+
console.log(currentBridgeService().status().manager === 'host-process' ? 'Oomi bridge restored with worker recovery for this host session. Reconnect after a cloud computer restart. Pairing and conversations preserved.' : 'Oomi bridge restored with automatic startup. Pairing and conversations preserved.');
|
|
4274
4282
|
}
|
|
4275
4283
|
}
|
|
4276
4284
|
|
|
@@ -4415,7 +4423,7 @@ async function verifyLocalAgent(state) {
|
|
|
4415
4423
|
if (state.localAgent) {
|
|
4416
4424
|
const reply = await runLocalAgent(state.localAgent, {
|
|
4417
4425
|
sessionKey: `oomi:verify:${randomUUID()}`, idempotencyKey: randomUUID(), history: [],
|
|
4418
|
-
message: state.runtime === 'grokbot' ? 'Oomi connection check.
|
|
4426
|
+
message: state.runtime === 'grokbot' ? 'Oomi connection check.' : 'Oomi connection check. Reply with OOMI_OK only. Do not use tools or change anything.',
|
|
4419
4427
|
});
|
|
4420
4428
|
if (state.runtime === 'grokbot' && reply.text.trim() !== 'OOMI_OK') throw new Error('Grok Bot did not return the expected verification response. No Auth Link was created.');
|
|
4421
4429
|
result = { verifiedAt: new Date().toISOString() };
|
|
@@ -4438,7 +4446,7 @@ async function connectLocalAgent(flags) {
|
|
|
4438
4446
|
const agentName = flags['agent-name'] === undefined ? undefined : validateAgentName(flags['agent-name']);
|
|
4439
4447
|
const runtime = String(flags.runtime || '');
|
|
4440
4448
|
if (!['openclaw', 'hermes', 'grokbot', 'command'].includes(runtime)) throw new Error('Specify --runtime for your own agent: openclaw, hermes, grokbot, or command. An unlisted brand may use an adapter; read oomi capabilities. Never select a different agent just because it is installed.');
|
|
4441
|
-
const service = currentBridgeService();
|
|
4449
|
+
const service = currentBridgeService(runtime);
|
|
4442
4450
|
const unmanaged = isTruthyFlag(flags['no-service']);
|
|
4443
4451
|
if (!unmanaged) {
|
|
4444
4452
|
service.preflight();
|
|
@@ -4459,6 +4467,7 @@ async function connectLocalAgent(flags) {
|
|
|
4459
4467
|
'agent-id': flags['agent-id'] || saved?.agentId,
|
|
4460
4468
|
'skills-dir': flags['skills-dir'] || saved?.skillsDir,
|
|
4461
4469
|
'grokbot-config': flags['grokbot-config'] || saved?.configFile,
|
|
4470
|
+
'grokbot-gateway': flags['grokbot-gateway'] || saved?.gatewayFile,
|
|
4462
4471
|
command: flags.command || (saved ? JSON.stringify(saved.command) : undefined),
|
|
4463
4472
|
profile: flags.profile ?? saved?.profile,
|
|
4464
4473
|
'hermes-home': saved?.hermesHome,
|
|
@@ -4508,6 +4517,7 @@ async function connectLocalAgent(flags) {
|
|
|
4508
4517
|
const child = unmanaged ? startBridgeDetachedProcess({ 'app-url': appUrl }) : (service.start(), {});
|
|
4509
4518
|
await waitForBridgeConnection(child);
|
|
4510
4519
|
console.log(unmanaged ? 'Automatic startup: off (--no-service); your host supervisor must manage recovery.'
|
|
4520
|
+
: service.status().manager === 'host-process' ? 'Worker recovery: enabled for this host session. After a cloud computer restart, rerun oomi connect; boot recovery is unavailable on this host.'
|
|
4511
4521
|
: `Automatic startup: ${service.config.platform === 'darwin' ? 'macOS login service' : 'Linux user service'}; restarts after worker exits.`);
|
|
4512
4522
|
const invite = await requestConnectInviteLink({ backendHttp: state.brokerHttp, appUrl, sessionKey: state.sessionKey, deviceToken: state.deviceToken });
|
|
4513
4523
|
refreshSelectedSkill(state, flags);
|
|
@@ -4540,7 +4550,7 @@ async function main() {
|
|
|
4540
4550
|
runtimes: {
|
|
4541
4551
|
openclaw: { interface: 'native gateway', connect: 'oomi connect --runtime openclaw' },
|
|
4542
4552
|
hermes: { interface: 'quiet CLI or existing desktop gateway', connect: 'oomi connect --runtime hermes --name NAME' },
|
|
4543
|
-
grokbot: { interface: '
|
|
4553
|
+
grokbot: { interface: 'local Grok Bot gateway plus private Oomi claim/reply inbox; existing webhook configurations retained', connect: 'oomi connect --runtime grokbot --name NAME --agent-id UUID', setup: 'oomi grokbot instructions --name NAME --agent-id UUID', config: 'existing private gateway.json on the bot host; no webhook credentials required for new connections', validation: 'same-bot claim/reply handshake required before pairing', recovery: 'system service where available; otherwise worker recovery within the current host session, requiring reconnect after host restart', approvals: 'remain in Grok Bot', photos: false },
|
|
4544
4554
|
command: { interface: 'adapter invoking the current agent through its supported CLI/API/SDK', connect: 'oomi connect --runtime command --name NAME --command JSON_ARGV', request: ['sessionKey','message','idempotencyKey','history','runtimeSessionId'], response: ['text','sessionId?'] }
|
|
4545
4555
|
},
|
|
4546
4556
|
runtimeSelection: { rule: 'Connect the current agent on its actual host, never another installed agent. A missing brand name is not proof of incompatibility.', unlisted: 'Inspect supported input and final-reply interfaces. Build a command adapter if available; otherwise report the specific missing host capability.', proof: 'Match identity and session, verify one real reply, then approve the Auth Link and test in Oomi.' },
|
|
@@ -4560,6 +4570,15 @@ async function main() {
|
|
|
4560
4570
|
if (command === 'objectives') return handleObjectivesCommand(readBridgeState(), subcommand || 'list', args.flags, args.positionals);
|
|
4561
4571
|
if (command === 'grokbot') {
|
|
4562
4572
|
const directory = connectionDirectory();
|
|
4573
|
+
if (subcommand === 'receive') {
|
|
4574
|
+
if (!args.flags.file) throw new Error('Use --file with JSON containing the exact current message.');
|
|
4575
|
+
const stat = fs.lstatSync(args.flags.file);
|
|
4576
|
+
if (!stat.isFile() || stat.size > 4 * 1024 * 1024) throw new Error('Message file is invalid or too large.');
|
|
4577
|
+
const input = JSON.parse(fs.readFileSync(args.flags.file, 'utf8'));
|
|
4578
|
+
const root = process.env.OOMI_CONNECTIONS_DIR || path.join(os.homedir(), '.oomi', 'connections');
|
|
4579
|
+
console.log(JSON.stringify(receiveGrokbotRequest(grokbotInboxRoots(root), args.flags['agent-id'], input.message), null, 2));
|
|
4580
|
+
return;
|
|
4581
|
+
}
|
|
4563
4582
|
if (!bridgeConnectionName) throw new Error('Specify --name for this Grok Bot connection.');
|
|
4564
4583
|
if (subcommand === 'instructions') { console.log(grokbotInstructions(bridgeConnectionName, args.flags['agent-id'] || process.env.OOMI_GROKBOT_AGENT_ID)); return; }
|
|
4565
4584
|
if (subcommand === 'status') { console.log(JSON.stringify(readConnectionSetup(directory) || { status: 'not_started' }, null, 2)); return; }
|
|
@@ -4578,14 +4597,15 @@ async function main() {
|
|
|
4578
4597
|
}
|
|
4579
4598
|
return;
|
|
4580
4599
|
}
|
|
4581
|
-
throw new Error('Use oomi grokbot instructions|status|claim|reply with --name.');
|
|
4600
|
+
throw new Error('Use oomi grokbot instructions|status|receive|claim|reply with --name.');
|
|
4582
4601
|
}
|
|
4583
4602
|
if (command === 'connect') {
|
|
4584
4603
|
if (args.flags.runtime !== 'grokbot') return connectLocalAgent(args.flags);
|
|
4585
4604
|
const saved = readBridgeState().localAgent;
|
|
4586
4605
|
const directory = connectionDirectory();
|
|
4587
|
-
const config = grokbotConfig({ ...args.flags, 'agent-id': args.flags['agent-id'] || saved?.agentId, 'grokbot-config': args.flags['grokbot-config'] || saved?.configFile }, directory);
|
|
4606
|
+
const config = grokbotConfig({ ...args.flags, 'agent-id': args.flags['agent-id'] || saved?.agentId, 'grokbot-config': args.flags['grokbot-config'] || saved?.configFile, 'grokbot-gateway': args.flags['grokbot-gateway'] || saved?.gatewayFile }, directory);
|
|
4588
4607
|
if (!args.flags['setup-worker']) {
|
|
4608
|
+
if (!isTruthyFlag(args.flags['no-service'])) currentBridgeService('grokbot').preflight();
|
|
4589
4609
|
console.log(JSON.stringify(queueConnectionSetup({ directory, agentId: config.agentId, cli: process.argv[1], args: process.argv.slice(2) }), null, 2));
|
|
4590
4610
|
return;
|
|
4591
4611
|
}
|
package/lib/connectionSetup.js
CHANGED
|
@@ -33,7 +33,7 @@ function queue({directory,agentId,cli,args}) {
|
|
|
33
33
|
const existing=readConnectionSetup(directory);
|
|
34
34
|
if(existing?.status==='running' && (existing.expiresAt>Date.now() || processExists(existing.pid))) {
|
|
35
35
|
if(existing.agentId!==agentId)throw new Error('This connection is being configured for a different agent.');
|
|
36
|
-
return {status:'queued',instruction:'Setup is already running. Finish this turn so Grok Bot can answer its
|
|
36
|
+
return {status:'queued',instruction:'Setup is already running. Finish this turn so Grok Bot can answer its connection check. Use oomi grokbot status later; do not wait in this same bot turn.'};
|
|
37
37
|
}
|
|
38
38
|
const job={id:randomUUID(),agentId,status:'running',startedAt:Date.now(),expiresAt:Date.now()+600000};
|
|
39
39
|
save(directory,job);
|
|
@@ -45,7 +45,7 @@ function queue({directory,agentId,cli,args}) {
|
|
|
45
45
|
if (child.pid) save(directory,{...job,pid:child.pid});
|
|
46
46
|
child.unref();
|
|
47
47
|
}finally{fs.closeSync(fd);}
|
|
48
|
-
return {status:'queued',instruction:'Finish this turn now so Grok Bot can answer its verification
|
|
48
|
+
return {status:'queued',instruction:'Finish this turn now so Grok Bot can answer its verification check. The reply command returns the Auth Link after setup succeeds. Queued is not connected. Use oomi grokbot status later if no link arrives.'};
|
|
49
49
|
}
|
|
50
50
|
export function requireConnectionSetup(directory,id,agentId) {
|
|
51
51
|
const job=readConnectionSetup(directory);
|
package/lib/grokbotRuntime.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
3
5
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
6
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
7
|
|
|
@@ -12,7 +14,7 @@ export class GrokbotError extends Error {
|
|
|
12
14
|
super(message); Object.assign(this, { code, notStarted, runtimeReady });
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
|
-
const unavailable = () => new GrokbotError('grokbot_unconfigured', 'Grok Bot needs its
|
|
17
|
+
const unavailable = () => new GrokbotError('grokbot_unconfigured', 'This existing Grok Bot webhook connection needs its valid private configuration. For a fresh connection, run oomi grokbot instructions on the bot host. Do not connect a different agent.', { notStarted: true });
|
|
16
18
|
function readJson(file, limit = 4 * 1024 * 1024) {
|
|
17
19
|
const stat = fs.lstatSync(file);
|
|
18
20
|
if (!stat.isFile() || stat.size > limit) throw new Error('Invalid local file.');
|
|
@@ -24,6 +26,13 @@ function replaceJson(file, value) { const tmp = `${file}.${randomUUID()}.tmp`; e
|
|
|
24
26
|
export function grokbotConfig(flags, directory) {
|
|
25
27
|
if (!NAME.test(flags.name || flags.connection || path.basename(directory))) throw unavailable();
|
|
26
28
|
const configFile = path.resolve(flags['grokbot-config'] || path.join(directory, 'grokbot.json'));
|
|
29
|
+
// Existing explicit webhook connections retain their configuration. Fresh
|
|
30
|
+
// connections use the same local gateway as the installed Grok Bot host.
|
|
31
|
+
if (!flags['grokbot-config'] && !fs.existsSync(configFile)) {
|
|
32
|
+
const agentId = flags['agent-id'] || process.env.OOMI_GROKBOT_AGENT_ID;
|
|
33
|
+
if (!UUID.test(agentId || '')) throw new GrokbotError('grokbot_identity', 'Specify this Grok Bot UUID with --agent-id. Never select whichever bot is active on the shared computer.', { notStarted: true });
|
|
34
|
+
return { runtime: 'grokbot', agentId, transport: 'gateway', gatewayFile: path.resolve(flags['grokbot-gateway'] || '/home/box/sand-data/gateway.json'), requestsDir: path.join(directory, 'grokbot-requests'), connection: flags.name || flags.connection || path.basename(directory), ...(flags['skills-dir'] ? { skillsDir: path.resolve(flags['skills-dir']) } : {}), cwd: path.resolve(flags.cwd || process.cwd()) };
|
|
35
|
+
}
|
|
27
36
|
const settings = loadSettings(configFile);
|
|
28
37
|
const agentId = flags['agent-id'] || process.env.OOMI_GROKBOT_AGENT_ID || settings.agentId;
|
|
29
38
|
if (!UUID.test(agentId || '') || settings.agentId !== agentId) throw new GrokbotError('grokbot_identity', 'Specify the current Grok Bot UUID in --agent-id and the matching private configuration. Never infer identity from an installed agent.', { notStarted: true });
|
|
@@ -40,19 +49,103 @@ function loadSettings(file) {
|
|
|
40
49
|
} catch { throw unavailable(); }
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
function settingsFor(config) {
|
|
53
|
+
return config.transport === 'gateway' ? { protocol: PROTOCOL, agentId: config.agentId, gatewayFile: config.gatewayFile } : loadSettings(config.configFile);
|
|
54
|
+
}
|
|
55
|
+
function gatewayFor(config, settings = settingsFor(config)) {
|
|
46
56
|
let gateway;
|
|
47
|
-
try {
|
|
48
|
-
|
|
57
|
+
try {
|
|
58
|
+
const file = settings.gatewayFile || '/home/box/sand-data/gateway.json';
|
|
59
|
+
const stat = fs.lstatSync(file);
|
|
60
|
+
if (!stat.isFile() || (process.platform !== 'win32' && ((stat.mode & 0o077) || stat.uid !== process.getuid()))) throw new Error();
|
|
61
|
+
gateway = readJson(file, 65536);
|
|
62
|
+
} catch { throw new GrokbotError('grokbot_host_unavailable', 'Run this connection on the Grok Bot host where its private local gateway.json is available.', { notStarted: true }); }
|
|
63
|
+
if (!['http', 'https'].includes(gateway.scheme) || !['127.0.0.1', 'localhost', '0.0.0.0', '::', '::1'].includes(gateway.host) || !Number.isInteger(gateway.port) || gateway.port < 1 || gateway.port > 65535 || typeof gateway.token !== 'string' || !gateway.token || /[\r\n]/.test(gateway.token)) throw new GrokbotError('grokbot_host_unavailable', 'Grok Bot requires a private loopback gateway on its own host.', { notStarted: true });
|
|
49
64
|
const host = ['::', '::1'].includes(gateway.host) ? '[::1]' : '127.0.0.1';
|
|
65
|
+
return { url: `${gateway.scheme}://${host}:${gateway.port}`, headers: { Authorization: `Bearer ${gateway.token}`, 'Content-Type': 'application/json' } };
|
|
66
|
+
}
|
|
67
|
+
export async function grokbotHealth(config, { fetchImpl = fetch } = {}) {
|
|
68
|
+
const settings = settingsFor(config);
|
|
69
|
+
if (settings.agentId !== config.agentId) throw unavailable();
|
|
70
|
+
const gateway = gatewayFor(config, settings);
|
|
50
71
|
try {
|
|
51
|
-
const response = await fetchImpl(`${gateway.
|
|
72
|
+
const response = await fetchImpl(`${gateway.url}/health`, { headers: gateway.headers, redirect: 'error', signal: AbortSignal.timeout(5000) });
|
|
52
73
|
const body = await response.json();
|
|
53
74
|
if (!response.ok || body.ok !== true || typeof body.isBusy !== 'boolean') throw new Error();
|
|
75
|
+
if (config.transport === 'gateway') {
|
|
76
|
+
const roster = await fetchImpl(`${gateway.url}/api/listAgents`, { method: 'POST', headers: gateway.headers, body: '{}', redirect: 'error', signal: AbortSignal.timeout(5000) });
|
|
77
|
+
const agents = await roster.json();
|
|
78
|
+
if (!roster.ok || !Array.isArray(agents)) throw new Error();
|
|
79
|
+
if (!agents.some(agent => agent.id === config.agentId)) throw new GrokbotError('grokbot_identity', 'This Grok Bot UUID is not available on this host. Check the selected bot; Oomi has not substituted another agent.', { notStarted: true });
|
|
80
|
+
}
|
|
54
81
|
return { isBusy: body.isBusy, needsApproval: body.busyOnlyAwaitingApproval === true, currentAgent: body.activeAgentId === config.agentId };
|
|
55
|
-
} catch
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof GrokbotError) throw error;
|
|
84
|
+
throw new GrokbotError('grokbot_host_unavailable', 'The Grok Bot host is unavailable or its local gateway is incompatible. Check its connection and permissions, then retry.', { notStarted: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const shellQuote = value => "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
88
|
+
const DELIVERY_SKILL = 'oomi-delivery';
|
|
89
|
+
export function installGrokbotDeliverySkill(config) {
|
|
90
|
+
const source = fs.readFileSync(new URL('../skills/oomi-delivery/SKILL.md', import.meta.url), 'utf8');
|
|
91
|
+
const root = path.join(path.dirname(config.gatewayFile), 'workflows');
|
|
92
|
+
// Only the host's existing private skill library is a supported install target.
|
|
93
|
+
// Never create a guessed provider directory or overwrite a user's skill.
|
|
94
|
+
try {
|
|
95
|
+
const stat = fs.lstatSync(root);
|
|
96
|
+
if (!stat.isDirectory() || (process.platform !== 'win32' && stat.uid !== process.getuid())) throw new Error();
|
|
97
|
+
const directory = path.join(root, DELIVERY_SKILL);
|
|
98
|
+
privateDirectory(directory);
|
|
99
|
+
if (!fs.lstatSync(directory).isDirectory()) throw new Error();
|
|
100
|
+
const file = path.join(directory, 'SKILL.md');
|
|
101
|
+
try { fs.writeFileSync(file, source, { mode: 0o600, flag: 'wx' }); }
|
|
102
|
+
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
103
|
+
const skill = fs.lstatSync(file);
|
|
104
|
+
if (!skill.isFile() || skill.size > 65536 || fs.readFileSync(file, 'utf8') !== source) throw new Error();
|
|
105
|
+
} catch {
|
|
106
|
+
throw new GrokbotError('grokbot_skill_unavailable', 'Oomi could not install its delivery skill in this host’s existing private skill library, or that skill has local changes. Repair the Oomi skill before reconnecting; no message was sent.', { notStarted: true });
|
|
107
|
+
}
|
|
108
|
+
const registry = config.deliveryRegistryDir || path.join(os.homedir(), '.oomi', 'grokbot-inboxes');
|
|
109
|
+
const inboxRoot = path.resolve(path.dirname(path.dirname(config.requestsDir)));
|
|
110
|
+
replaceJson(path.join(registry, createHash('sha256').update(inboxRoot).digest('hex') + '.json'), { root: inboxRoot });
|
|
111
|
+
return { id: DELIVERY_SKILL, body: source.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '').trim() };
|
|
112
|
+
}
|
|
113
|
+
export function grokbotInboxRoots(defaultRoot, registry = path.join(os.homedir(), '.oomi', 'grokbot-inboxes')) {
|
|
114
|
+
const roots = new Set([path.resolve(defaultRoot)]);
|
|
115
|
+
if (fs.existsSync(registry)) for (const entry of fs.readdirSync(registry, { withFileTypes: true })) {
|
|
116
|
+
if (!entry.isFile() || !/^[0-9a-f]{64}\.json$/.test(entry.name)) continue;
|
|
117
|
+
const saved = readJson(path.join(registry, entry.name), 65536);
|
|
118
|
+
if (typeof saved.root !== 'string' || !path.isAbsolute(saved.root)) throw new Error('Invalid Oomi inbox registration.');
|
|
119
|
+
if (createHash('sha256').update(saved.root).digest('hex') + '.json' !== entry.name) throw new Error('Invalid Oomi inbox registration identity.');
|
|
120
|
+
roots.add(saved.root);
|
|
121
|
+
}
|
|
122
|
+
return [...roots];
|
|
123
|
+
}
|
|
124
|
+
async function gatewayMessage(config, request, id, { fetchImpl, signal, skillTimeoutMs, pollMs }) {
|
|
125
|
+
const skill = installGrokbotDeliverySkill(config);
|
|
126
|
+
const gateway = gatewayFor(config);
|
|
127
|
+
const end = Date.now() + skillTimeoutMs;
|
|
128
|
+
// The provider watches skill files asynchronously. A readable file is not
|
|
129
|
+
// evidence that the selected bot has loaded it yet (verified on a real host).
|
|
130
|
+
let ready = false;
|
|
131
|
+
do {
|
|
132
|
+
if (signal?.aborted) break;
|
|
133
|
+
try {
|
|
134
|
+
const response = await fetchImpl(`${gateway.url}/api/getAgentWorkflows`, { method: 'POST', headers: gateway.headers, body: JSON.stringify({ id: config.agentId }), redirect: 'error', signal: AbortSignal.timeout(5000) });
|
|
135
|
+
const skills = await response.json();
|
|
136
|
+
ready = response.ok && Array.isArray(skills) && skills.some(item => item.id === skill.id && typeof item.body === 'string' && item.body.trim() === skill.body);
|
|
137
|
+
if (ready) break;
|
|
138
|
+
} catch { break; }
|
|
139
|
+
if (Date.now() < end) await delay(Math.min(pollMs, Math.max(1, end-Date.now())));
|
|
140
|
+
} while (Date.now() < end);
|
|
141
|
+
if (!ready || signal?.aborted) throw new GrokbotError('grokbot_skill_unavailable', 'This bot has not loaded the Oomi delivery skill yet. Reconnect after its skill library refreshes; no message was sent.', { notStarted: true });
|
|
142
|
+
return {
|
|
143
|
+
agentId: config.agentId, prompt: request.message, clientNonce: id,
|
|
144
|
+
richText: JSON.stringify({ type: 'doc', content: [{ type: 'paragraph', content: [
|
|
145
|
+
{ type: 'workflowReference', attrs: { id: skill.id, name: 'Oomi' } },
|
|
146
|
+
{ type: 'text', text: request.message }
|
|
147
|
+
] }] })
|
|
148
|
+
};
|
|
56
149
|
}
|
|
57
150
|
export async function waitForGrokbotIdle(config, { timeoutMs = 300000 } = {}) {
|
|
58
151
|
const end = Date.now() + timeoutMs;
|
|
@@ -88,12 +181,12 @@ export function completedGrokbotTurn(config, request) {
|
|
|
88
181
|
return resultFor(config, request, id);
|
|
89
182
|
}
|
|
90
183
|
|
|
91
|
-
// The
|
|
92
|
-
//
|
|
93
|
-
export async function runGrokbotTurn(config, request, { signal, timeoutMs = 180000, fetchImpl = fetch, pollMs = 250 } = {}) {
|
|
184
|
+
// The local gateway (or an existing webhook) wakes this exact bot. Acceptance
|
|
185
|
+
// is not completion: both transports return through the same private claim/reply inbox.
|
|
186
|
+
export async function runGrokbotTurn(config, request, { signal, timeoutMs = 180000, fetchImpl = fetch, pollMs = 250, skillTimeoutMs = 15000 } = {}) {
|
|
94
187
|
if (request.imagePaths?.length) throw new GrokbotError('grokbot_photos', 'Grok Bot photos are not supported through Oomi yet. Send the photo in Grok Bot.', { notStarted: true, runtimeReady: true });
|
|
95
188
|
if (!request.sessionKey || !request.idempotencyKey || typeof request.message !== 'string' || !request.message.trim()) throw new Error('A session, message, and idempotency key are required.');
|
|
96
|
-
const settings =
|
|
189
|
+
const settings = settingsFor(config);
|
|
97
190
|
if (settings.agentId !== config.agentId) throw unavailable();
|
|
98
191
|
const id = requestId(config, request), file = requestFile(config.requestsDir, id);
|
|
99
192
|
if (fs.existsSync(file)) {
|
|
@@ -105,6 +198,8 @@ export async function runGrokbotTurn(config, request, { signal, timeoutMs = 1800
|
|
|
105
198
|
const health = await grokbotHealth(config, { fetchImpl });
|
|
106
199
|
if (health.isBusy || health.needsApproval) throw new GrokbotError('grokbot_busy', health.needsApproval ? 'Grok Bot is waiting for approval in its own app. Resolve it before sending another message.' : 'Grok Bot is busy. Finish the current task, then retry. This message was not sent.', { notStarted: true, runtimeReady: true });
|
|
107
200
|
if (signal?.aborted) throw new GrokbotError('grokbot_cancelled', 'Grok Bot message cancelled before sending.', { notStarted: true, runtimeReady: true });
|
|
201
|
+
// Prepare before persisting a dispatch: unsupported hosts fail without sending.
|
|
202
|
+
const delivery = config.transport === 'gateway' ? await gatewayMessage(config, request, id, { fetchImpl, signal, skillTimeoutMs, pollMs }) : null;
|
|
108
203
|
const record = { ...request, protocol: PROTOCOL, requestId: id, agentId: config.agentId, connection: config.connection, fingerprint: requestFingerprint(request), status: 'dispatching', expiresAt: Date.now() + timeoutMs, imagePaths: undefined };
|
|
109
204
|
if (Buffer.byteLength(JSON.stringify(record)) > 4 * 1024 * 1024) throw new GrokbotError('grokbot_request_size', 'This conversation is too large for the Grok Bot inbox. Start a new Oomi conversation; no message was sent.', { notStarted: true, runtimeReady: true });
|
|
110
205
|
// An atomic claim also gates duplicate webhooks/parallel workers. A persisted
|
|
@@ -129,16 +224,20 @@ export async function runGrokbotTurn(config, request, { signal, timeoutMs = 1800
|
|
|
129
224
|
const timer = setTimeout(abort, timeoutMs);
|
|
130
225
|
let accepted = false, safeToRelease = false;
|
|
131
226
|
try {
|
|
132
|
-
const
|
|
227
|
+
const local = config.transport === 'gateway';
|
|
228
|
+
const gateway = local ? gatewayFor(config, settings) : null;
|
|
229
|
+
const response = await fetchImpl(local ? `${gateway.url}/api/sendPrompt` : settings.webhook.url, { method: 'POST', redirect: 'error', headers: local ? gateway.headers : { Authorization: `Bearer ${settings.webhook.token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(local ? delivery : { protocol: PROTOCOL, requestId: id, agentId: config.agentId, connection: config.connection }), signal: controller.signal });
|
|
133
230
|
if (response.status !== 200) {
|
|
134
231
|
if ([400,401,403,404,409,422,429].includes(response.status)) {
|
|
135
232
|
replaceJson(file, { ...record, status: 'not_started' });
|
|
136
233
|
safeToRelease = true;
|
|
137
|
-
throw new GrokbotError('grokbot_webhook_rejected', 'Grok Bot did not accept this message. Check the routine, webhook key, host approvals, or usage limits, then retry.', { notStarted: true, runtimeReady: true });
|
|
234
|
+
throw new GrokbotError(local ? 'grokbot_gateway_rejected' : 'grokbot_webhook_rejected', local ? 'Grok Bot did not accept this message. Check its host connection, approvals, and usage limits, then retry.' : 'Grok Bot did not accept this message. Check the routine, webhook key, host approvals, or usage limits, then retry.', { notStarted: true, runtimeReady: true });
|
|
138
235
|
}
|
|
139
236
|
throw new Error();
|
|
140
237
|
}
|
|
141
|
-
|
|
238
|
+
if (local) {
|
|
239
|
+
if ((await response.json()).accepted !== true) throw new Error();
|
|
240
|
+
} else await response.body?.cancel();
|
|
142
241
|
accepted = true;
|
|
143
242
|
while (Date.now() < record.expiresAt && !controller.signal.aborted) {
|
|
144
243
|
const result = resultFor(config, request, id);
|
|
@@ -166,7 +265,40 @@ export function claimGrokbotRequest(directory, id, agentId) {
|
|
|
166
265
|
if (fs.existsSync(requestFile(directory, id, 'reply.json'))) return { status: 'already_completed', requestId: id };
|
|
167
266
|
try { exclusiveJson(requestFile(directory, id, 'claim.json'), { agentId, claimedAt: Date.now() }); }
|
|
168
267
|
catch (err) { if (err.code === 'EEXIST') return { status: 'already_claimed', requestId: id, instruction: 'Do not repeat any actions. Inspect the original run.' }; throw err; }
|
|
169
|
-
return { status: 'claimed', protocol: PROTOCOL, requestId: id, agentId, sessionKey: request.sessionKey, runtimeSessionId: request.runtimeSessionId, message: request.message, history: request.history || [], connection: request.connection };
|
|
268
|
+
return { status: 'claimed', protocol: PROTOCOL, requestId: id, agentId, sessionKey: request.sessionKey, runtimeSessionId: request.runtimeSessionId, message: request.message, history: request.history || [], connection: request.connection, verification: request.sessionKey.startsWith('oomi:verify:') };
|
|
269
|
+
}
|
|
270
|
+
// The visible user message and trusted runtime UUID jointly select the inbox
|
|
271
|
+
// item. Never pick the newest item, infer identity from an active bot, or guess
|
|
272
|
+
// when identical messages are pending in multiple connections.
|
|
273
|
+
export function receiveGrokbotRequest(roots, agentId, message) {
|
|
274
|
+
if (!Array.isArray(roots)) roots = [roots];
|
|
275
|
+
if (!UUID.test(agentId || '') || typeof message !== 'string' || !message.trim()) throw new Error('Use your trusted bot UUID and the exact current message.');
|
|
276
|
+
const matches = [];
|
|
277
|
+
for (const root of new Set(roots.map(value => path.resolve(value)))) {
|
|
278
|
+
const connections = fs.existsSync(root) ? fs.readdirSync(root, { withFileTypes: true }) : [];
|
|
279
|
+
if (connections.length > 512) throw new Error('Too many connections to select a delivery safely.');
|
|
280
|
+
for (const connection of connections) {
|
|
281
|
+
if (!connection.isDirectory() || !NAME.test(connection.name)) continue;
|
|
282
|
+
const directory = path.join(root, connection.name, 'grokbot-requests');
|
|
283
|
+
if (!fs.existsSync(directory) || !fs.lstatSync(directory).isDirectory()) continue;
|
|
284
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
285
|
+
if (!entry.isFile() || !/^[0-9a-f]{64}\.json$/.test(entry.name)) continue;
|
|
286
|
+
const request = readJson(path.join(directory, entry.name));
|
|
287
|
+
if (request.protocol !== PROTOCOL || request.agentId !== agentId || request.message !== message || request.expiresAt <= Date.now() || ['not_started', 'cancelled'].includes(request.status)) continue;
|
|
288
|
+
const id = entry.name.slice(0, -5);
|
|
289
|
+
if (request.requestId !== id || request.connection !== connection.name) throw new Error('Invalid Oomi delivery identity.');
|
|
290
|
+
if (!fs.existsSync(requestFile(directory, id, 'reply.json'))) matches.push({ root, directory, id, connection: connection.name });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (matches.length === 0) return { status: 'no_pending_delivery', instruction: 'Do not perform or repeat the task. This message has no matching active Oomi delivery.' };
|
|
295
|
+
if (matches.length !== 1) return { status: 'ambiguous_delivery', instruction: 'Do not perform the task. More than one active Oomi delivery matches this message.' };
|
|
296
|
+
const { root, directory, id, connection } = matches[0];
|
|
297
|
+
const claim = claimGrokbotRequest(directory, id, agentId);
|
|
298
|
+
if (claim.status !== 'claimed') return claim;
|
|
299
|
+
const cli = fileURLToPath(new URL('../bin/oomi-ai.js', import.meta.url));
|
|
300
|
+
const replyFile = requestFile(directory, id, 'answer.json');
|
|
301
|
+
return { ...claim, replyFile, replyCommand: `env OOMI_CONNECTIONS_DIR=${shellQuote(path.resolve(root))} ${shellQuote(process.execPath)} ${shellQuote(cli)} grokbot reply --name ${connection} --agent-id ${agentId} --request ${id} --file ${shellQuote(replyFile)}` };
|
|
170
302
|
}
|
|
171
303
|
export function replyGrokbotRequest(directory, id, agentId, input) {
|
|
172
304
|
const request = checkedRequest(directory, id, agentId);
|
|
@@ -183,5 +315,14 @@ export function replyGrokbotRequest(directory, id, agentId, input) {
|
|
|
183
315
|
|
|
184
316
|
export function grokbotInstructions(connection, agentId) {
|
|
185
317
|
if (!NAME.test(connection || '') || !UUID.test(agentId || '')) throw new Error('Use --name CONNECTION --agent-id YOUR_GROK_BOT_UUID.');
|
|
186
|
-
return `
|
|
318
|
+
return `On THIS Grok Bot's Linux host, run:
|
|
319
|
+
oomi connect --runtime grokbot --name ${connection} --agent-id ${agentId} --agent-name "Your name" --app-url https://www.oomi.ai
|
|
320
|
+
Use your existing name and exact bot UUID from your runtime context. Do not choose the active bot on a shared computer or another installed agent.
|
|
321
|
+
|
|
322
|
+
The package uses the host's existing private local gateway.json. Do not ask the user for a webhook URL or key, create a routine, copy gateway credentials, or configure another agent. If the host gateway is absent or incompatible, report that specific error; never invent an interface.
|
|
323
|
+
|
|
324
|
+
Setup returns queued. Finish your current turn immediately so the background verification can reach this bot. The package installs its private Oomi delivery skill. Deliveries show the original message with a small skill tag; the skill uses the private receive/reply inbox. Continue only when receive returns status claimed. Return exactly OOMI_OK for verification; use normal host permissions for user tasks. The reply command returns one Auth Link after verification, pairing and bridge subscription succeed. Show that link to the user. Queued or accepted is not connected. If setup is pending, finish the turn; use oomi grokbot status --name ${connection} later.
|
|
325
|
+
|
|
326
|
+
Existing private webhook configurations remain supported. Fresh connections need no grokbot.json. On hosts without systemd, Oomi supervises worker exits during this host session; after a whole cloud computer restart, rerun the same connect command. No new pairing or model verification is needed for a saved connection. Use oomi status --name ${connection} --json to inspect recovery.
|
|
327
|
+
`;
|
|
187
328
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
7
|
+
|
|
8
|
+
function read(file) {
|
|
9
|
+
try {
|
|
10
|
+
const stat = fs.lstatSync(file);
|
|
11
|
+
if (!stat.isFile() || stat.size > 65536 || (stat.mode & 0o077) || stat.uid !== process.getuid()) throw new Error('Invalid private service file.');
|
|
12
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
13
|
+
} catch (error) { if (error.code === 'ENOENT') return null; throw error; }
|
|
14
|
+
}
|
|
15
|
+
function save(file, data) {
|
|
16
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
17
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
18
|
+
fs.writeFileSync(tmp, JSON.stringify(data), { mode: 0o600, flag: 'wx' });
|
|
19
|
+
fs.renameSync(tmp, file);
|
|
20
|
+
}
|
|
21
|
+
function identity(pid) {
|
|
22
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return null;
|
|
23
|
+
try {
|
|
24
|
+
process.kill(pid, 0);
|
|
25
|
+
if (process.platform === 'linux') {
|
|
26
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
27
|
+
return fs.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim() + ':' + stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19];
|
|
28
|
+
}
|
|
29
|
+
const result = spawnSync('ps', ['-p', String(pid), '-o', 'lstart='], { encoding: 'utf8', timeout: 2000 });
|
|
30
|
+
return result.status === 0 ? result.stdout.trim() || null : null;
|
|
31
|
+
} catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
function owner(file) {
|
|
34
|
+
const state = read(`${file}.owner`);
|
|
35
|
+
return state && state.identity && identity(state.pid) === state.identity ? state : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A host-session supervisor, explicitly distinct from an OS boot service. It
|
|
39
|
+
// recovers worker exits on container hosts without modifying their init system.
|
|
40
|
+
export function createHostProcessService(base) {
|
|
41
|
+
const config = { ...base, file: path.join(base.directory, 'oomi-process-service.json'), target: base.label };
|
|
42
|
+
const availability = () => ({ available: config.platform === 'linux', reason: config.platform === 'linux' ? null : 'Host-session supervision requires Linux.' });
|
|
43
|
+
function preflight() {
|
|
44
|
+
if (!availability().available) throw new Error(availability().reason);
|
|
45
|
+
const existing = read(config.file);
|
|
46
|
+
if (existing && existing.directory !== config.directory) throw new Error('This supervisor belongs to another connection.');
|
|
47
|
+
if (config.args[1].split(path.sep).includes('_npx')) throw new Error('Install oomi-ai before starting host-session supervision; an npx cache is temporary.');
|
|
48
|
+
for (const file of config.args.slice(0, 2)) fs.accessSync(file, fs.constants.R_OK);
|
|
49
|
+
}
|
|
50
|
+
function update(fields) {
|
|
51
|
+
preflight();
|
|
52
|
+
const existing = read(config.file);
|
|
53
|
+
if (!existing) throw new Error('Connection supervisor is not installed. Run oomi bridge repair.');
|
|
54
|
+
save(config.file, { ...existing, ...fields });
|
|
55
|
+
}
|
|
56
|
+
function status() {
|
|
57
|
+
const saved = read(config.file), supervisor = owner(config.file), child = read(`${config.file}.status`);
|
|
58
|
+
const current = child?.instance === supervisor?.instance;
|
|
59
|
+
return { ...availability(), manager: 'host-process', connection: config.name, target: config.target, file: config.file,
|
|
60
|
+
installed: Boolean(saved), enabled: saved?.enabled === true, loaded: Boolean(supervisor),
|
|
61
|
+
running: Boolean(saved?.enabled && current && child?.childIdentity && identity(child.childPid) === child.childIdentity),
|
|
62
|
+
pid: current ? child?.childPid || null : null, startsAt: 'host-session', persistsWithoutLogin: true, startsAfterHostRestart: false };
|
|
63
|
+
}
|
|
64
|
+
function start({ rewrite = false } = {}) {
|
|
65
|
+
update({ enabled: true, ...(rewrite ? { args: config.args, environment: config.environment, generation: randomUUID() } : {}) });
|
|
66
|
+
if (owner(config.file)) return;
|
|
67
|
+
fs.mkdirSync(path.dirname(config.logPath), { recursive: true, mode: 0o700 });
|
|
68
|
+
const fd = fs.openSync(config.logPath, 'a', 0o600);
|
|
69
|
+
fs.fchmodSync(fd, 0o600);
|
|
70
|
+
try {
|
|
71
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), '--run', config.file], { detached: true, stdio: ['ignore', fd, fd], env: { ...process.env, OOMI_CONNECTION: config.name } });
|
|
72
|
+
child.on('error', () => {}); // The connection readiness check reports failure.
|
|
73
|
+
child.unref();
|
|
74
|
+
} finally { fs.closeSync(fd); }
|
|
75
|
+
}
|
|
76
|
+
function install({ noStart = false } = {}) {
|
|
77
|
+
preflight();
|
|
78
|
+
save(config.file, { directory: config.directory, args: config.args, environment: config.environment, enabled: false, generation: randomUUID() });
|
|
79
|
+
if (!noStart) start();
|
|
80
|
+
}
|
|
81
|
+
const stop = () => { if (read(config.file)) update({ enabled: false }); };
|
|
82
|
+
const restart = () => { if (!status().enabled || !owner(config.file)) throw new Error('Repair the connection supervisor before restarting.'); update({ generation: randomUUID() }); };
|
|
83
|
+
const uninstall = () => { stop(); if (fs.existsSync(config.file)) fs.unlinkSync(config.file); };
|
|
84
|
+
return { config, availability, preflight, status, install, start, stop, restart, uninstall };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function supervise(file) {
|
|
88
|
+
const mine = { pid: process.pid, identity: identity(process.pid), instance: randomUUID() };
|
|
89
|
+
const lock = `${file}.owner`;
|
|
90
|
+
// Publish a complete identity atomically: concurrent starts must never read a
|
|
91
|
+
// half-written owner file and mistake a live supervisor for a stale one.
|
|
92
|
+
const publication = `${lock}.${mine.instance}.tmp`;
|
|
93
|
+
fs.writeFileSync(publication, JSON.stringify(mine), { flag: 'wx', mode: 0o600 });
|
|
94
|
+
try {
|
|
95
|
+
try { fs.linkSync(publication, lock); }
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error.code !== 'EEXIST' || owner(file)) return;
|
|
98
|
+
try { fs.unlinkSync(lock); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
99
|
+
try { fs.linkSync(publication, lock); } catch { return; }
|
|
100
|
+
}
|
|
101
|
+
} finally { fs.unlinkSync(publication); }
|
|
102
|
+
let ending = false, child = null, generation, nextStart = 0, stoppingAt = 0;
|
|
103
|
+
const stopChild = () => { if (child && !stoppingAt) { stoppingAt = Date.now(); child.kill('SIGTERM'); } };
|
|
104
|
+
const shutdown = () => { ending = true; stopChild(); };
|
|
105
|
+
process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown);
|
|
106
|
+
try {
|
|
107
|
+
while (true) {
|
|
108
|
+
const saved = read(file);
|
|
109
|
+
if (read(lock)?.instance !== mine.instance) ending = true;
|
|
110
|
+
if (!saved) ending = true;
|
|
111
|
+
if (ending || !saved?.enabled || (child && generation !== saved.generation)) stopChild();
|
|
112
|
+
if (child && stoppingAt && Date.now() - stoppingAt > 15000) child.kill('SIGKILL');
|
|
113
|
+
if (ending && !child) break;
|
|
114
|
+
if (!ending && saved.enabled && !child && Date.now() >= nextStart) {
|
|
115
|
+
if (saved.directory !== path.dirname(file) || !Array.isArray(saved.args) || saved.args.length < 2) throw new Error('Invalid supervisor definition.');
|
|
116
|
+
generation = saved.generation; stoppingAt = 0;
|
|
117
|
+
const worker = spawn(saved.args[0], saved.args.slice(1), { cwd: saved.directory, env: { ...process.env, ...saved.environment }, stdio: ['ignore', 'inherit', 'inherit'] });
|
|
118
|
+
child = worker;
|
|
119
|
+
const finished = () => { if (child === worker) { child = null; nextStart = Date.now() + 1000; stoppingAt = 0; } };
|
|
120
|
+
worker.once('error', finished); worker.once('exit', finished);
|
|
121
|
+
}
|
|
122
|
+
save(`${file}.status`, { instance: mine.instance, childPid: child?.pid || null, childIdentity: child?.pid ? identity(child.pid) : null });
|
|
123
|
+
await delay(250);
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
stopChild();
|
|
127
|
+
// A broken/removed definition must not strand the worker after its owner exits.
|
|
128
|
+
const deadline = Date.now() + 15000;
|
|
129
|
+
while (child && Date.now() < deadline) await delay(100);
|
|
130
|
+
if (child) child.kill('SIGKILL');
|
|
131
|
+
if (read(lock)?.instance === mine.instance) fs.unlinkSync(lock);
|
|
132
|
+
if (read(`${file}.status`)?.instance === mine.instance) fs.unlinkSync(`${file}.status`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) && process.argv[2] === '--run') {
|
|
136
|
+
await supervise(path.resolve(process.argv[3]));
|
|
137
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oomi-ai",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "Connect OpenClaw, Hermes, Grok Bot, and other agents to Oomi with verified runtime adapters",
|
|
5
5
|
"bin": {
|
|
6
6
|
"oomi": "bin/oomi-ai.js"
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"url": "https://github.com/crispcode-io/oomi/issues"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
|
-
"check": "node --check bin/oomi-ai.js && node --check lib/bridgeService.js && node --check lib/bridgeMaintenance.js && node --check lib/localAgent.js && node --check lib/hermesGateway.js && node --check lib/grokbotRuntime.js && node --check lib/connectionSetup.js",
|
|
52
|
+
"check": "node --check bin/oomi-ai.js && node --check lib/bridgeService.js && node --check lib/bridgeMaintenance.js && node --check lib/localAgent.js && node --check lib/hermesGateway.js && node --check lib/grokbotRuntime.js && node --check lib/connectionSetup.js && node --check lib/processSupervisor.js",
|
|
53
53
|
"test": "node --test test/*.test.mjs"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
@@ -81,6 +81,8 @@
|
|
|
81
81
|
"lib/bridgeMaintenance.js",
|
|
82
82
|
"lib/hermesGateway.js",
|
|
83
83
|
"lib/grokbotRuntime.js",
|
|
84
|
-
"lib/connectionSetup.js"
|
|
84
|
+
"lib/connectionSetup.js",
|
|
85
|
+
"lib/processSupervisor.js",
|
|
86
|
+
"skills/oomi-delivery/SKILL.md"
|
|
85
87
|
]
|
|
86
88
|
}
|
package/skills/oomi/SKILL.md
CHANGED
|
@@ -7,32 +7,32 @@ description: Connect the current OpenClaw, Hermes, Grok Bot, or other agent to O
|
|
|
7
7
|
|
|
8
8
|
Connect the agent the user is speaking to. Identify your runtime, bot/profile, and actual host first. Computer access does not mean you run on that computer. Never substitute another installed agent.
|
|
9
9
|
|
|
10
|
-
## Choose a connection by interface (oomi-ai 0.8.
|
|
10
|
+
## Choose a connection by interface (oomi-ai 0.8.5+)
|
|
11
11
|
|
|
12
12
|
Run setup on the host where **this agent** runs. A Grok Bot with access to someone's Mac still runs on its Grok Bot host; connecting an OpenClaw agent on that Mac does not connect the Grok Bot. No new model API key or replacement bot is needed.
|
|
13
13
|
|
|
14
14
|
1. Identify yourself from your current runtime context: bot/profile identity, host, and conversation behavior. Do not guess from installed executables or names.
|
|
15
|
-
2. Run `oomi capabilities`. OpenClaw and Hermes have native adapters. Grok Bot
|
|
15
|
+
2. Run `oomi capabilities`. OpenClaw and Hermes have native adapters. Grok Bot uses its own local gateway through the adapter described below. An unlisted brand is not automatically unsupported: inspect its documented message input **and completed-reply** interfaces (CLI, API, SDK, webhook, or inbound channel).
|
|
16
16
|
3. For another runtime, use `--runtime command` to wrap those interfaces and return `{text, sessionId?}` only when the same agent has finished. A health endpoint or an accepted/queued response is not a final reply. If no supported input or reply interface exists, state that specific missing capability; never invent one or select another agent.
|
|
17
17
|
4. Preserve identity, memories and host approvals. Say whether you resume an exact existing conversation or keep an Oomi conversation with the same agent. Verify a real reply before sharing an Auth Link; verify delivery in Oomi before claiming completion.
|
|
18
18
|
|
|
19
19
|
### Grok Bot: connect the existing bot
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
On the Grok Bot host, run `oomi grokbot instructions --name YOUR_CONNECTION --agent-id YOUR_BOT_UUID`. Follow the printed fixed routine instruction with the bot's normal routine tools. Save the platform-issued HTTPS webhook URL and key to the printed private `grokbot.json` (mode 0600). Never paste credentials into chat. Then run:
|
|
21
|
+
On **this bot's own Linux host**, install/update the package through the normal host approval flow, then run:
|
|
24
22
|
|
|
25
23
|
```bash
|
|
26
24
|
oomi connect --runtime grokbot --name YOUR_CONNECTION --agent-id YOUR_BOT_UUID --agent-name "Your name" --app-url https://www.oomi.ai
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
Use your exact bot UUID from your runtime context. Never choose whichever bot happens to be active on a shared computer. Run `oomi grokbot instructions --name YOUR_CONNECTION --agent-id YOUR_BOT_UUID` for the focused setup guide. The package uses the host's existing private local gateway; **do not ask the user to copy a webhook URL/key or create a routine**. Fresh connections require no `grokbot.json`. Existing explicit webhook configurations remain supported. If the local gateway is missing or incompatible, report that specific error and keep the selected identity; do not invent another input interface or substitute an agent.
|
|
28
|
+
|
|
29
|
+
Setup returns **queued**, then waits in the background. Finish your current bot turn immediately so the verification can execute; waiting for your own verification in that turn deadlocks the host. The package installs its private Oomi delivery skill and waits until this bot has loaded it. Incoming chat shows the original message with a small skill tag; the skill claims the private request and submits its final reply. Missing or locally modified skills fail before sending; do not replace them with instruction walls in chat. Continue only for `status: claimed`. A verification answer must be exactly `OOMI_OK`. The reply command reports one Auth Link only after verification, pairing and bridge subscription succeed. Show the Auth Link to the user. If pending, finish the turn and use `oomi grokbot status --name YOUR_CONNECTION` later. Queued is not connected. Acceptance by the host is not a completed answer.
|
|
30
30
|
|
|
31
|
-
Use
|
|
31
|
+
Use explicit `--connection YOUR_CONNECTION` for Oomi account commands: a shared host does not inherit the bridge's environment. Text and voice transcripts are supported; photos are rejected. Oomi supplies conversation history; exact Grok Bot desktop/DM conversation continuation is not guaranteed. Host approvals remain in Grok Bot. Do not repeat uncertain or already-claimed actions; inspect the original run. Reconnect reuses verification and pairing; `oomi verify --name YOUR_CONNECTION` explicitly queues a new model verification.
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Oomi uses systemd user services where available. On Linux hosts without systemd, it automatically supervises bridge worker exits for the current host session. **After the whole cloud computer restarts, rerun the same connect command.** This fallback cannot start itself at boot. Use `oomi status --name YOUR_CONNECTION --json` to inspect recovery. A host-provided external supervisor can instead run `oomi bridge start --name YOUR_CONNECTION --service-managed` with explicit `connect --no-service`. Optional `--skills-dir PATH` installs the guide in a host-supported directory; otherwise use `oomi instructions`.
|
|
34
34
|
|
|
35
|
-
The adapter
|
|
35
|
+
The direct adapter targets the installed Grok Bot local gateway contract. An incompatible provider update fails closed. Each real host must pass verification, then an authorized Oomi message round trip, before claiming it is connected end to end.
|
|
36
36
|
|
|
37
37
|
## Discover and use Oomi (oomi-ai 0.7.0+)
|
|
38
38
|
|
|
@@ -2,32 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Connect the agent the user is speaking to. Identify your runtime, bot/profile, and actual host first. Computer access does not mean you run on that computer. Never substitute another installed agent.
|
|
4
4
|
|
|
5
|
-
## Choose a connection by interface (oomi-ai 0.8.
|
|
5
|
+
## Choose a connection by interface (oomi-ai 0.8.5+)
|
|
6
6
|
|
|
7
7
|
Run setup on the host where **this agent** runs. A Grok Bot with access to someone's Mac still runs on its Grok Bot host; connecting an OpenClaw agent on that Mac does not connect the Grok Bot. No new model API key or replacement bot is needed.
|
|
8
8
|
|
|
9
9
|
1. Identify yourself from your current runtime context: bot/profile identity, host, and conversation behavior. Do not guess from installed executables or names.
|
|
10
|
-
2. Run `oomi capabilities`. OpenClaw and Hermes have native adapters. Grok Bot
|
|
10
|
+
2. Run `oomi capabilities`. OpenClaw and Hermes have native adapters. Grok Bot uses its own local gateway through the adapter described below. An unlisted brand is not automatically unsupported: inspect its documented message input **and completed-reply** interfaces (CLI, API, SDK, webhook, or inbound channel).
|
|
11
11
|
3. For another runtime, use `--runtime command` to wrap those interfaces and return `{text, sessionId?}` only when the same agent has finished. A health endpoint or an accepted/queued response is not a final reply. If no supported input or reply interface exists, state that specific missing capability; never invent one or select another agent.
|
|
12
12
|
4. Preserve identity, memories and host approvals. Say whether you resume an exact existing conversation or keep an Oomi conversation with the same agent. Verify a real reply before sharing an Auth Link; verify delivery in Oomi before claiming completion.
|
|
13
13
|
|
|
14
14
|
### Grok Bot: connect the existing bot
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
On the Grok Bot host, run `oomi grokbot instructions --name YOUR_CONNECTION --agent-id YOUR_BOT_UUID`. Follow the printed fixed routine instruction with the bot's normal routine tools. Save the platform-issued HTTPS webhook URL and key to the printed private `grokbot.json` (mode 0600). Never paste credentials into chat. Then run:
|
|
16
|
+
On **this bot's own Linux host**, install/update the package through the normal host approval flow, then run:
|
|
19
17
|
|
|
20
18
|
```bash
|
|
21
19
|
oomi connect --runtime grokbot --name YOUR_CONNECTION --agent-id YOUR_BOT_UUID --agent-name "Your name" --app-url https://www.oomi.ai
|
|
22
20
|
```
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
Use your exact bot UUID from your runtime context. Never choose whichever bot happens to be active on a shared computer. Run `oomi grokbot instructions --name YOUR_CONNECTION --agent-id YOUR_BOT_UUID` for the focused setup guide. The package uses the host's existing private local gateway; **do not ask the user to copy a webhook URL/key or create a routine**. Fresh connections require no `grokbot.json`. Existing explicit webhook configurations remain supported. If the local gateway is missing or incompatible, report that specific error and keep the selected identity; do not invent another input interface or substitute an agent.
|
|
23
|
+
|
|
24
|
+
Setup returns **queued**, then waits in the background. Finish your current bot turn immediately so the verification can execute; waiting for your own verification in that turn deadlocks the host. The package installs its private Oomi delivery skill and waits until this bot has loaded it. Incoming chat shows the original message with a small skill tag; the skill claims the private request and submits its final reply. Missing or locally modified skills fail before sending; do not replace them with instruction walls in chat. Continue only for `status: claimed`. A verification answer must be exactly `OOMI_OK`. The reply command reports one Auth Link only after verification, pairing and bridge subscription succeed. Show the Auth Link to the user. If pending, finish the turn and use `oomi grokbot status --name YOUR_CONNECTION` later. Queued is not connected. Acceptance by the host is not a completed answer.
|
|
25
25
|
|
|
26
|
-
Use
|
|
26
|
+
Use explicit `--connection YOUR_CONNECTION` for Oomi account commands: a shared host does not inherit the bridge's environment. Text and voice transcripts are supported; photos are rejected. Oomi supplies conversation history; exact Grok Bot desktop/DM conversation continuation is not guaranteed. Host approvals remain in Grok Bot. Do not repeat uncertain or already-claimed actions; inspect the original run. Reconnect reuses verification and pairing; `oomi verify --name YOUR_CONNECTION` explicitly queues a new model verification.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Oomi uses systemd user services where available. On Linux hosts without systemd, it automatically supervises bridge worker exits for the current host session. **After the whole cloud computer restarts, rerun the same connect command.** This fallback cannot start itself at boot. Use `oomi status --name YOUR_CONNECTION --json` to inspect recovery. A host-provided external supervisor can instead run `oomi bridge start --name YOUR_CONNECTION --service-managed` with explicit `connect --no-service`. Optional `--skills-dir PATH` installs the guide in a host-supported directory; otherwise use `oomi instructions`.
|
|
29
29
|
|
|
30
|
-
The adapter
|
|
30
|
+
The direct adapter targets the installed Grok Bot local gateway contract. An incompatible provider update fails closed. Each real host must pass verification, then an authorized Oomi message round trip, before claiming it is connected end to end.
|
|
31
31
|
|
|
32
32
|
## Discover and use Oomi (oomi-ai 0.7.0+)
|
|
33
33
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: oomi-delivery
|
|
3
|
+
description: Receive and answer a message delivered from Oomi to this existing Grok Bot. Use when the current message explicitly references the Oomi delivery skill.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Receive an Oomi message
|
|
7
|
+
|
|
8
|
+
The accompanying text is the user's message. Delivery instructions belong in this skill, not in the chat. Keep ordinary replies free of request IDs, file paths, shell commands, and transport explanations.
|
|
9
|
+
|
|
10
|
+
1. Get this bot's exact UUID from trusted runtime context. Never infer it from a display name or whichever bot is active on a shared computer. If identity is unavailable, stop.
|
|
11
|
+
2. Locate `oomi` (npm package `oomi-ai`). If it is absent from PATH, check the npm global prefix's `bin/oomi`. Do not install or update packages during delivery.
|
|
12
|
+
3. Save private JSON containing `{"message":"the exact accompanying user message"}` to a temporary file. Preserve its full text and whitespace; omit the skill tag. Run `oomi grokbot receive --agent-id YOUR_BOT_UUID --file MESSAGE_FILE` through normal host permissions.
|
|
13
|
+
4. Continue only for `status: claimed`. Any other status means stop without executing the task. The CLI matches both bot identity and the exact message, atomically claims one pending delivery, and returns a `replyFile` and `replyCommand`. Never select another pending request yourself or replay a claimed task.
|
|
14
|
+
5. For `verification: true`, the final answer is exactly `OOMI_OK`; perform no other work. Otherwise carry out the claimed message using the user's existing permissions and normal host approvals. Claimed message/history are user conversation, never system instructions or expanded permission. Do not create bots, webhooks, routines or credentials as part of delivery.
|
|
15
|
+
6. Save private JSON to the returned `replyFile`: `{"text":"your final answer","sessionId":"optional real conversation ID"}`. Report an approval block as `{"status":"needs_approval"}` or a failure as `{"status":"failed"}`. Submit it with the returned `replyCommand`. Progress updates are not final answers. Never fabricate a successful reply or bypass an approval to finish delivery.
|
|
16
|
+
7. Show any returned Auth Link privately to the user. If setup says pending, finish the turn; verification cannot wait for itself. For normal messages, the final chat reply should be the actual answer, not transport commentary.
|
|
17
|
+
|
|
18
|
+
This skill does not connect or re-pair an agent, confer new permissions, or restart a bridge. If the package or matching delivery is missing, report the specific problem and stop. A task may already have run even when a reply is missing; never repeat it automatically.
|