@odla-ai/cli 0.36.1 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.cjs +77 -54
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-4QJ5NS64.js → chunk-XFIFFYNH.js} +70 -55
- package/dist/chunk-XFIFFYNH.js.map +1 -0
- package/dist/{cli-BFGY7F6X.js → cli-OH6PCYZ3.js} +2 -2
- package/dist/index.cjs +69 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-4QJ5NS64.js.map +0 -1
- /package/dist/{cli-BFGY7F6X.js.map → cli-OH6PCYZ3.js.map} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runCli
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XFIFFYNH.js";
|
|
5
5
|
import {
|
|
6
6
|
exitCodeFor
|
|
7
7
|
} from "./chunk-UKLSRQ5J.js";
|
|
@@ -9,4 +9,4 @@ export {
|
|
|
9
9
|
exitCodeFor,
|
|
10
10
|
runCli
|
|
11
11
|
};
|
|
12
|
-
//# sourceMappingURL=cli-
|
|
12
|
+
//# sourceMappingURL=cli-OH6PCYZ3.js.map
|
package/dist/index.cjs
CHANGED
|
@@ -638,6 +638,7 @@ var SCOPE_PURPOSE = {
|
|
|
638
638
|
"app:config:write": "apply or inspect one revision-bound configuration operation for an app you own",
|
|
639
639
|
"platform:runbook:write": "read and edit all of odla's operational runbooks, including admin-visible content",
|
|
640
640
|
"app:device:enroll": "enrol this machine so it can mint its own short-lived credentials without asking you again",
|
|
641
|
+
"platform:device:enroll:extended": "enrol this machine with a credential that outlives the seven-day owner ceiling \u2014 a durable secret on a machine you are vouching for",
|
|
641
642
|
"platform:ai:policy:write": "change System AI model routing",
|
|
642
643
|
"platform:ai:policy:read": "read System AI model routing",
|
|
643
644
|
"platform:ai:credential:write": "replace a stored AI provider key",
|
|
@@ -7009,7 +7010,7 @@ function createCodeRuntimeControlClient(options) {
|
|
|
7009
7010
|
throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
|
|
7010
7011
|
}
|
|
7011
7012
|
const request3 = options.fetch ?? fetch;
|
|
7012
|
-
const
|
|
7013
|
+
const call3 = async (path, body, timeoutMs = requestTimeoutMs) => {
|
|
7013
7014
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
7014
7015
|
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
7015
7016
|
let response2;
|
|
@@ -7039,17 +7040,17 @@ function createCodeRuntimeControlClient(options) {
|
|
|
7039
7040
|
return {
|
|
7040
7041
|
heartbeat: async (version, capabilities) => {
|
|
7041
7042
|
validateHeartbeat(version, capabilities);
|
|
7042
|
-
return parseSnapshot(await
|
|
7043
|
+
return parseSnapshot(await call3("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
|
|
7043
7044
|
},
|
|
7044
7045
|
acknowledge: async (commandId, result) => {
|
|
7045
7046
|
if (!/^ccmd_[0-9a-f]{32}$/.test(commandId)) throw new TypeError("invalid Code runtime command id");
|
|
7046
|
-
await
|
|
7047
|
+
await call3(`/registry/code/runtime/commands/${commandId}/ack`, result);
|
|
7047
7048
|
},
|
|
7048
7049
|
source: async (sessionId) => parseSource(
|
|
7049
|
-
await
|
|
7050
|
+
await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
|
|
7050
7051
|
),
|
|
7051
7052
|
infer: async (sessionId, inference) => {
|
|
7052
|
-
const value2 = record5(await
|
|
7053
|
+
const value2 = record5(await call3(
|
|
7053
7054
|
`/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`,
|
|
7054
7055
|
inference,
|
|
7055
7056
|
modelRequestTimeoutMs
|
|
@@ -7060,11 +7061,11 @@ function createCodeRuntimeControlClient(options) {
|
|
|
7060
7061
|
return value2;
|
|
7061
7062
|
},
|
|
7062
7063
|
review: async (sessionId, review) => parseReview(
|
|
7063
|
-
await
|
|
7064
|
+
await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review, modelRequestTimeoutMs)
|
|
7064
7065
|
),
|
|
7065
7066
|
submitCandidate: async (sessionId, checkpointId, verification) => {
|
|
7066
7067
|
if (!/^cpoint_[0-9a-f]{32}$/.test(checkpointId)) throw new TypeError("invalid Code checkpoint id");
|
|
7067
|
-
return parseCandidate(await
|
|
7068
|
+
return parseCandidate(await call3(
|
|
7068
7069
|
`/registry/code/runtime/sessions/${validSessionId(sessionId)}/candidates`,
|
|
7069
7070
|
{ checkpointId, verification }
|
|
7070
7071
|
));
|
|
@@ -7074,21 +7075,21 @@ function createCodeRuntimeControlClient(options) {
|
|
|
7074
7075
|
if (!/^[A-Za-z0-9._:-]{1,120}$/.test(eventId) || !event || typeof event !== "object" || new TextEncoder().encode(serialized).byteLength > 24e3) {
|
|
7075
7076
|
throw new TypeError("invalid Code session event");
|
|
7076
7077
|
}
|
|
7077
|
-
await
|
|
7078
|
+
await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
|
|
7078
7079
|
},
|
|
7079
7080
|
recallMemories: async (sessionId, subjects, limit) => {
|
|
7080
|
-
const response2 = await
|
|
7081
|
+
const response2 = await call3(
|
|
7081
7082
|
`/registry/code/runtime/sessions/${validSessionId(sessionId)}/recall`,
|
|
7082
7083
|
{ subjects: [...subjects], limit }
|
|
7083
7084
|
);
|
|
7084
7085
|
return Array.isArray(response2.memories) ? response2.memories : [];
|
|
7085
7086
|
},
|
|
7086
7087
|
rememberMemory: async (sessionId, memory) => {
|
|
7087
|
-
await
|
|
7088
|
+
await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
|
|
7088
7089
|
},
|
|
7089
7090
|
reportSessionFailure: async (sessionId, message2) => {
|
|
7090
7091
|
if (!message2.trim() || message2.length > 2e3) throw new TypeError("invalid Code session failure");
|
|
7091
|
-
await
|
|
7092
|
+
await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
|
|
7092
7093
|
}
|
|
7093
7094
|
};
|
|
7094
7095
|
}
|
|
@@ -7977,7 +7978,7 @@ var SYSTEM_PROMPT_FOR = {
|
|
|
7977
7978
|
};
|
|
7978
7979
|
function codeSkill(opts) {
|
|
7979
7980
|
let seq = 0;
|
|
7980
|
-
const
|
|
7981
|
+
const call3 = async (tool, input, signal) => {
|
|
7981
7982
|
const startedAt = Date.now();
|
|
7982
7983
|
const response2 = await opts.broker.execute(
|
|
7983
7984
|
{ lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
|
|
@@ -7999,7 +8000,7 @@ function codeSkill(opts) {
|
|
|
7999
8000
|
},
|
|
8000
8001
|
additionalProperties: false
|
|
8001
8002
|
},
|
|
8002
|
-
handler: (input, ctx) =>
|
|
8003
|
+
handler: (input, ctx) => call3("sandbox.read", input, ctx.signal)
|
|
8003
8004
|
};
|
|
8004
8005
|
const applyPatch = {
|
|
8005
8006
|
name: "odla_apply_git_diff",
|
|
@@ -8010,7 +8011,7 @@ function codeSkill(opts) {
|
|
|
8010
8011
|
properties: { patch: { type: "string", minLength: 1, maxLength: 262144 } },
|
|
8011
8012
|
additionalProperties: false
|
|
8012
8013
|
},
|
|
8013
|
-
handler: (input, ctx) =>
|
|
8014
|
+
handler: (input, ctx) => call3("sandbox.apply_patch", input, ctx.signal)
|
|
8014
8015
|
};
|
|
8015
8016
|
const runRecipe = {
|
|
8016
8017
|
name: "odla_run_recipe",
|
|
@@ -8021,7 +8022,7 @@ function codeSkill(opts) {
|
|
|
8021
8022
|
properties: { recipeId: { type: "string", minLength: 1, maxLength: 120, pattern: "^[a-zA-Z0-9._:-]+$" } },
|
|
8022
8023
|
additionalProperties: false
|
|
8023
8024
|
},
|
|
8024
|
-
handler: (input, ctx) =>
|
|
8025
|
+
handler: (input, ctx) => call3("sandbox.run_recipe", input, ctx.signal)
|
|
8025
8026
|
};
|
|
8026
8027
|
const listFiles2 = {
|
|
8027
8028
|
name: "odla_list",
|
|
@@ -8034,7 +8035,7 @@ function codeSkill(opts) {
|
|
|
8034
8035
|
},
|
|
8035
8036
|
additionalProperties: false
|
|
8036
8037
|
},
|
|
8037
|
-
handler: (input, ctx) =>
|
|
8038
|
+
handler: (input, ctx) => call3("sandbox.list", input, ctx.signal)
|
|
8038
8039
|
};
|
|
8039
8040
|
const searchFiles = {
|
|
8040
8041
|
name: "odla_search",
|
|
@@ -8050,7 +8051,7 @@ function codeSkill(opts) {
|
|
|
8050
8051
|
},
|
|
8051
8052
|
additionalProperties: false
|
|
8052
8053
|
},
|
|
8053
|
-
handler: (input, ctx) =>
|
|
8054
|
+
handler: (input, ctx) => call3("sandbox.search", input, ctx.signal)
|
|
8054
8055
|
};
|
|
8055
8056
|
const graphTool = (name, tool, description, required) => ({
|
|
8056
8057
|
name,
|
|
@@ -8061,7 +8062,7 @@ function codeSkill(opts) {
|
|
|
8061
8062
|
properties: { query: { type: "string", maxLength: 512 } },
|
|
8062
8063
|
additionalProperties: false
|
|
8063
8064
|
},
|
|
8064
|
-
handler: (input, ctx) =>
|
|
8065
|
+
handler: (input, ctx) => call3(tool, input, ctx.signal)
|
|
8065
8066
|
});
|
|
8066
8067
|
const orientation = [
|
|
8067
8068
|
graphTool(
|
|
@@ -8100,9 +8101,9 @@ async function runCodeAgent(options) {
|
|
|
8100
8101
|
lease: options.lease,
|
|
8101
8102
|
workspaceDir: options.workspaceDir,
|
|
8102
8103
|
surface,
|
|
8103
|
-
onToolCall: (
|
|
8104
|
-
toolCalls.push(
|
|
8105
|
-
options.onToolCall?.(
|
|
8104
|
+
onToolCall: (call3) => {
|
|
8105
|
+
toolCalls.push(call3);
|
|
8106
|
+
options.onToolCall?.(call3);
|
|
8106
8107
|
}
|
|
8107
8108
|
});
|
|
8108
8109
|
const compaction = options.compaction === void 0 ? (0, import_ai4.keepRecentExchanges)({ whenInputTokensExceed: 12e4, keep: 3 }) : options.compaction;
|
|
@@ -10414,7 +10415,7 @@ Usage:
|
|
|
10414
10415
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
10415
10416
|
odla-ai security run [target] --self --ack-redacted-source
|
|
10416
10417
|
odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--no-open] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
10417
|
-
odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--email <odla-account>] [--no-open] [--json]
|
|
10418
|
+
odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--json]
|
|
10418
10419
|
odla-ai device list [--email <odla-account>] [--json]
|
|
10419
10420
|
odla-ai device revoke <device-id> [--email <odla-account>] [--json]
|
|
10420
10421
|
odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
|
|
@@ -12883,9 +12884,15 @@ async function provisionEnvCredentials(opts) {
|
|
|
12883
12884
|
if (o11yToken) {
|
|
12884
12885
|
opts.stdout.log(`${opts.env}: reusing local o11y ingest token`);
|
|
12885
12886
|
} else {
|
|
12886
|
-
o11yToken = await issueO11yToken(opts);
|
|
12887
|
-
|
|
12888
|
-
|
|
12887
|
+
o11yToken = await issueO11yToken(opts) ?? void 0;
|
|
12888
|
+
if (o11yToken) {
|
|
12889
|
+
credentials = save(opts, credentials, tenantId, { ...dbKey ? { dbKey } : {}, o11yToken });
|
|
12890
|
+
opts.stdout.log(`${opts.env}: ${opts.rotateO11y ? "rotated" : "issued"} o11y ingest token`);
|
|
12891
|
+
} else {
|
|
12892
|
+
opts.stdout.log(
|
|
12893
|
+
`${opts.env}: o11y token already exists and its shown-once value is not on this machine \u2014 leaving it alone (a co-owner provisioned this env). Nothing was rotated, and this env's running Worker keeps its token. To replace it deliberately \u2014 which invalidates the token on whatever is deployed \u2014 run "odla-ai provision --rotate-o11y-token --push-secrets".`
|
|
12894
|
+
);
|
|
12895
|
+
}
|
|
12889
12896
|
}
|
|
12890
12897
|
}
|
|
12891
12898
|
return save(opts, credentials, tenantId, {
|
|
@@ -12939,11 +12946,7 @@ async function issueO11yToken(opts) {
|
|
|
12939
12946
|
`${opts.cfg.platformUrl}/o11y/${encodeURIComponent(opts.cfg.app.id)}/token${suffix}?env=${encodeURIComponent(opts.env)}`,
|
|
12940
12947
|
{ method: "POST", headers: { authorization: `Bearer ${opts.developerToken}` } }
|
|
12941
12948
|
);
|
|
12942
|
-
if (res.status === 409 && !opts.rotateO11y)
|
|
12943
|
-
throw new Error(
|
|
12944
|
-
`o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --push-secrets" to install a separate runtime credential without rotating siblings`
|
|
12945
|
-
);
|
|
12946
|
-
}
|
|
12949
|
+
if (res.status === 409 && !opts.rotateO11y) return null;
|
|
12947
12950
|
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
|
|
12948
12951
|
const body = await res.json();
|
|
12949
12952
|
if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
|
|
@@ -13505,6 +13508,24 @@ function renderAdvisories(out, advisories, env = process.env) {
|
|
|
13505
13508
|
}
|
|
13506
13509
|
}
|
|
13507
13510
|
|
|
13511
|
+
// src/device-ttl.ts
|
|
13512
|
+
var OWNER_DEVICE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
13513
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
13514
|
+
var UNITS = { d: DAY_MS, w: 7 * DAY_MS, y: 365 * DAY_MS };
|
|
13515
|
+
function parseDeviceTtl(raw) {
|
|
13516
|
+
if (raw === void 0 || raw === true) return void 0;
|
|
13517
|
+
const text3 = String(raw).trim().toLowerCase();
|
|
13518
|
+
if (!text3) return void 0;
|
|
13519
|
+
if (text3 === "forever" || text3 === "never") return 100 * 365 * DAY_MS;
|
|
13520
|
+
const match = /^(\d+)\s*([dwy])$/.exec(text3);
|
|
13521
|
+
if (!match) {
|
|
13522
|
+
throw new Error(
|
|
13523
|
+
`--device-ttl expects a duration like 30d, 6w, 2y, or "forever" (got "${raw}")`
|
|
13524
|
+
);
|
|
13525
|
+
}
|
|
13526
|
+
return Number(match[1]) * UNITS[match[2]];
|
|
13527
|
+
}
|
|
13528
|
+
|
|
13508
13529
|
// src/device-command.ts
|
|
13509
13530
|
var import_node_fs18 = require("fs");
|
|
13510
13531
|
var import_node_path17 = require("path");
|
|
@@ -13524,7 +13545,17 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
|
|
|
13524
13545
|
const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
|
|
13525
13546
|
const apps = (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
|
|
13526
13547
|
if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026]");
|
|
13527
|
-
const
|
|
13548
|
+
const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
|
|
13549
|
+
const extended = deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
|
|
13550
|
+
const token = await scopedToken2(
|
|
13551
|
+
parsed,
|
|
13552
|
+
deps,
|
|
13553
|
+
cfg,
|
|
13554
|
+
doFetch,
|
|
13555
|
+
out,
|
|
13556
|
+
`odla CLI (enroll ${name})`,
|
|
13557
|
+
extended ? "platform:device:enroll:extended" : "app:device:enroll"
|
|
13558
|
+
);
|
|
13528
13559
|
const response2 = await doFetch(`${cfg.platformUrl}/registry/devices`, {
|
|
13529
13560
|
method: "POST",
|
|
13530
13561
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
@@ -13532,7 +13563,8 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
|
|
|
13532
13563
|
name,
|
|
13533
13564
|
platform: import_node_process15.default.platform,
|
|
13534
13565
|
appIds: apps,
|
|
13535
|
-
...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {}
|
|
13566
|
+
...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {},
|
|
13567
|
+
...deviceTtlMs === void 0 ? {} : { deviceTtlMs }
|
|
13536
13568
|
})
|
|
13537
13569
|
});
|
|
13538
13570
|
const body = await response2.json().catch(() => ({}));
|
|
@@ -13585,12 +13617,12 @@ async function revoke(parsed, deps, cfg, doFetch, out, json) {
|
|
|
13585
13617
|
out.error(`device: revoked ${deviceId}; every credential it minted is revoked with it`);
|
|
13586
13618
|
if (json) out.log(JSON.stringify({ deviceId, revoked: true }, null, 2));
|
|
13587
13619
|
}
|
|
13588
|
-
async function scopedToken2(parsed, deps, cfg, doFetch, out, label) {
|
|
13620
|
+
async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app:device:enroll") {
|
|
13589
13621
|
const { credentials } = await resolveOperatorContext(parsed, { allowMissingConfig: true });
|
|
13590
13622
|
const scopedTokenFile = credentials.scopedTokenFile;
|
|
13591
13623
|
return getScopedPlatformToken({
|
|
13592
13624
|
platform: cfg.platformUrl,
|
|
13593
|
-
scope
|
|
13625
|
+
scope,
|
|
13594
13626
|
email: stringOpt(parsed.options.email),
|
|
13595
13627
|
label,
|
|
13596
13628
|
fetch: doFetch,
|
|
@@ -13849,12 +13881,7 @@ ${counts.created} created, ${counts.updated} updated, ${counts.unchanged} unchan
|
|
|
13849
13881
|
);
|
|
13850
13882
|
}
|
|
13851
13883
|
async function upsert(ctx, r, visibility) {
|
|
13852
|
-
const
|
|
13853
|
-
ctx,
|
|
13854
|
-
"GET",
|
|
13855
|
-
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(r.slug)}&limit=1`
|
|
13856
|
-
);
|
|
13857
|
-
const found = page2.records[0];
|
|
13884
|
+
const found = await bySlug(ctx, r.slug).catch(() => null);
|
|
13858
13885
|
if (!found) {
|
|
13859
13886
|
await call(ctx, "POST", "/runbook", {
|
|
13860
13887
|
appId: ctx.appId,
|
|
@@ -14259,13 +14286,7 @@ async function runbookAsk(ctx, question, all) {
|
|
|
14259
14286
|
ctx.out.log(JSDOC_POINTER);
|
|
14260
14287
|
}
|
|
14261
14288
|
async function runbookComment(ctx, slug, body) {
|
|
14262
|
-
const
|
|
14263
|
-
ctx,
|
|
14264
|
-
"GET",
|
|
14265
|
-
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
14266
|
-
);
|
|
14267
|
-
const found = page2.records[0];
|
|
14268
|
-
if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
14289
|
+
const found = await bySlug(ctx, slug);
|
|
14269
14290
|
await call(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
|
|
14270
14291
|
ctx.out.log(`commented on ${slug} (v${found.version})`);
|
|
14271
14292
|
}
|
|
@@ -14316,13 +14337,7 @@ var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
|
|
|
14316
14337
|
|
|
14317
14338
|
// src/runbook-edit-flow.ts
|
|
14318
14339
|
async function editRunbook(ctx, slug, deps = {}) {
|
|
14319
|
-
const
|
|
14320
|
-
ctx,
|
|
14321
|
-
"GET",
|
|
14322
|
-
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
14323
|
-
);
|
|
14324
|
-
const found = page2.records[0];
|
|
14325
|
-
if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
14340
|
+
const found = await bySlug(ctx, slug);
|
|
14326
14341
|
ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
|
|
14327
14342
|
const body = await editText(found.body, slug, deps);
|
|
14328
14343
|
return body === null ? null : { body, expectedVersion: found.version };
|