@01.software/cli 0.10.6 → 0.11.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/index.js +760 -70
- package/dist/index.js.map +1 -1
- package/dist/mcp/.01-cli-mcp-build.json +1 -1
- package/dist/mcp/{chunk-2ULP5WQH.js → chunk-VEFJZ6VK.js} +736 -361
- package/dist/mcp/chunk-VEFJZ6VK.js.map +1 -0
- package/dist/mcp/http.js +1 -1
- package/dist/mcp/stdio.js +1 -1
- package/dist/mcp/vercel.js +735 -360
- package/package.json +4 -3
- package/dist/mcp/chunk-2ULP5WQH.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -129,27 +129,32 @@ ${entry}
|
|
|
129
129
|
// src/lib/output.ts
|
|
130
130
|
import pc from "picocolors";
|
|
131
131
|
|
|
132
|
-
// src/
|
|
133
|
-
var
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
132
|
+
// ../errors/src/admin-error.ts
|
|
133
|
+
var ADMIN_ERROR_CODES = {
|
|
134
|
+
permission: [
|
|
135
|
+
"tenant_mismatch",
|
|
136
|
+
"account_suspended",
|
|
137
|
+
"feature_disabled",
|
|
138
|
+
"role_denied",
|
|
139
|
+
"credential_invalid",
|
|
140
|
+
"pat_tenant_unpinned",
|
|
141
|
+
"publishable_key_mismatch",
|
|
142
|
+
"scope_denied"
|
|
143
|
+
],
|
|
144
|
+
degraded: [
|
|
145
|
+
"redis_unavailable",
|
|
146
|
+
"provider_unavailable",
|
|
147
|
+
"rate_limited"
|
|
148
|
+
],
|
|
149
|
+
network: [
|
|
150
|
+
"upstream_timeout",
|
|
151
|
+
"upstream_5xx",
|
|
152
|
+
"dns_failure"
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
var PERMISSION_CODES = ADMIN_ERROR_CODES.permission;
|
|
156
|
+
var DEGRADED_CODES = ADMIN_ERROR_CODES.degraded;
|
|
157
|
+
var NETWORK_CODES = ADMIN_ERROR_CODES.network;
|
|
153
158
|
function isPermissionCode(code) {
|
|
154
159
|
return PERMISSION_CODES.includes(code);
|
|
155
160
|
}
|
|
@@ -159,7 +164,10 @@ function isDegradedCode(code) {
|
|
|
159
164
|
function isNetworkCode(code) {
|
|
160
165
|
return NETWORK_CODES.includes(code);
|
|
161
166
|
}
|
|
162
|
-
function
|
|
167
|
+
function isUnknownAdminError(err) {
|
|
168
|
+
return err.type === "validation" && err.code === "unknown";
|
|
169
|
+
}
|
|
170
|
+
function classifyAdminError(err) {
|
|
163
171
|
if (err && typeof err === "object") {
|
|
164
172
|
const obj = err;
|
|
165
173
|
if (typeof obj.code === "string") {
|
|
@@ -167,9 +175,6 @@ function classifyError(err) {
|
|
|
167
175
|
if (isPermissionCode(code)) {
|
|
168
176
|
return { type: "permission", code };
|
|
169
177
|
}
|
|
170
|
-
if (code === "auth_error" || code === "permission_error") {
|
|
171
|
-
return { type: "permission", code: "credential_invalid" };
|
|
172
|
-
}
|
|
173
178
|
if (isDegradedCode(code)) {
|
|
174
179
|
const out = { type: "degraded", code };
|
|
175
180
|
if (typeof obj.retryAfter === "number") {
|
|
@@ -189,33 +194,6 @@ function classifyError(err) {
|
|
|
189
194
|
}
|
|
190
195
|
return out;
|
|
191
196
|
}
|
|
192
|
-
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
193
|
-
const status = typeof obj.status === "number" ? obj.status : void 0;
|
|
194
|
-
if (name === "ConfigError" || name === "AuthError" || name === "PermissionError" || status === 401 || status === 403) {
|
|
195
|
-
return { type: "permission", code: "credential_invalid" };
|
|
196
|
-
}
|
|
197
|
-
if (name === "NetworkError" || name === "TimeoutError" || status === 408 || status === 503) {
|
|
198
|
-
return { type: "network", code: "upstream_timeout" };
|
|
199
|
-
}
|
|
200
|
-
if (name === "GoneError" || status === 404) {
|
|
201
|
-
return {
|
|
202
|
-
type: "validation",
|
|
203
|
-
code: "not_found",
|
|
204
|
-
detail: typeof obj.message === "string" ? { message: obj.message } : void 0
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
if (name === "UsageLimitError" || status === 429) {
|
|
208
|
-
const out = { type: "degraded", code: "rate_limited" };
|
|
209
|
-
if (typeof obj.retryAfter === "number") out.retryAfter = obj.retryAfter;
|
|
210
|
-
return out;
|
|
211
|
-
}
|
|
212
|
-
if (name === "ValidationError" || status === 400 || status === 422) {
|
|
213
|
-
return {
|
|
214
|
-
type: "validation",
|
|
215
|
-
code: "invalid_argument",
|
|
216
|
-
detail: typeof obj.message === "string" ? { message: obj.message } : void 0
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
197
|
}
|
|
220
198
|
return {
|
|
221
199
|
type: "validation",
|
|
@@ -223,6 +201,48 @@ function classifyError(err) {
|
|
|
223
201
|
detail: { message: err instanceof Error ? err.message : String(err) }
|
|
224
202
|
};
|
|
225
203
|
}
|
|
204
|
+
|
|
205
|
+
// src/lib/admin-error.ts
|
|
206
|
+
function classifyCliExtensions(err) {
|
|
207
|
+
if (!err || typeof err !== "object") return null;
|
|
208
|
+
const obj = err;
|
|
209
|
+
if (obj.code === "auth_error" || obj.code === "permission_error") {
|
|
210
|
+
return { type: "permission", code: "credential_invalid" };
|
|
211
|
+
}
|
|
212
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
213
|
+
const status = typeof obj.status === "number" ? obj.status : void 0;
|
|
214
|
+
if (name === "ConfigError" || name === "AuthError" || name === "PermissionError" || status === 401 || status === 403) {
|
|
215
|
+
return { type: "permission", code: "credential_invalid" };
|
|
216
|
+
}
|
|
217
|
+
if (name === "NetworkError" || name === "TimeoutError" || status === 408 || status === 503) {
|
|
218
|
+
return { type: "network", code: "upstream_timeout" };
|
|
219
|
+
}
|
|
220
|
+
if (name === "GoneError" || status === 404) {
|
|
221
|
+
return {
|
|
222
|
+
type: "validation",
|
|
223
|
+
code: "not_found",
|
|
224
|
+
detail: typeof obj.message === "string" ? { message: obj.message } : void 0
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (name === "UsageLimitError" || status === 429) {
|
|
228
|
+
const out = { type: "degraded", code: "rate_limited" };
|
|
229
|
+
if (typeof obj.retryAfter === "number") out.retryAfter = obj.retryAfter;
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
if (name === "ValidationError" || status === 400 || status === 422) {
|
|
233
|
+
return {
|
|
234
|
+
type: "validation",
|
|
235
|
+
code: "invalid_argument",
|
|
236
|
+
detail: typeof obj.message === "string" ? { message: obj.message } : void 0
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
function classifyError(err) {
|
|
242
|
+
const classified = classifyAdminError(err);
|
|
243
|
+
if (!isUnknownAdminError(classified)) return classified;
|
|
244
|
+
return classifyCliExtensions(err) ?? classified;
|
|
245
|
+
}
|
|
226
246
|
function adminErrorExitCode(err) {
|
|
227
247
|
if (err.code === "unknown") return 1;
|
|
228
248
|
switch (err.type) {
|
|
@@ -1312,7 +1332,7 @@ async function exchangeCode(code) {
|
|
|
1312
1332
|
}
|
|
1313
1333
|
}
|
|
1314
1334
|
function startAuthServer(options) {
|
|
1315
|
-
return new Promise((
|
|
1335
|
+
return new Promise((resolve3, reject) => {
|
|
1316
1336
|
const server = createServer((req, res) => {
|
|
1317
1337
|
if (!req.url) {
|
|
1318
1338
|
res.writeHead(400).end();
|
|
@@ -1387,7 +1407,7 @@ Logged in successfully!`));
|
|
|
1387
1407
|
);
|
|
1388
1408
|
cleanup(4);
|
|
1389
1409
|
}, TIMEOUT_MS);
|
|
1390
|
-
|
|
1410
|
+
resolve3({ port: addr.port, cleanup });
|
|
1391
1411
|
});
|
|
1392
1412
|
server.on("error", (err) => {
|
|
1393
1413
|
reject(err);
|
|
@@ -1831,6 +1851,22 @@ Prerequisites:
|
|
|
1831
1851
|
import { COLLECTIONS as COLLECTIONS3 } from "@01.software/sdk";
|
|
1832
1852
|
|
|
1833
1853
|
// src/lib/agent-output.ts
|
|
1854
|
+
var PLAN_AGENT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
1855
|
+
"PLAN_EXPIRED",
|
|
1856
|
+
"PLAN_MISMATCH",
|
|
1857
|
+
"PLAN_STALE",
|
|
1858
|
+
"UNSUPPORTED_OPERATION"
|
|
1859
|
+
]);
|
|
1860
|
+
function agentPlanError(code, message) {
|
|
1861
|
+
const error = new Error(message);
|
|
1862
|
+
Object.assign(error, {
|
|
1863
|
+
type: "validation",
|
|
1864
|
+
code: code.toLowerCase(),
|
|
1865
|
+
agentCode: code,
|
|
1866
|
+
detail: { message, agentCode: code }
|
|
1867
|
+
});
|
|
1868
|
+
return error;
|
|
1869
|
+
}
|
|
1834
1870
|
function stringifyAgentJson(data, options = {}) {
|
|
1835
1871
|
return options.pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
|
|
1836
1872
|
}
|
|
@@ -1862,6 +1898,10 @@ function isRawNotFound(error) {
|
|
|
1862
1898
|
function agentErrorExitCode(code) {
|
|
1863
1899
|
switch (code) {
|
|
1864
1900
|
case "INVALID_INPUT":
|
|
1901
|
+
case "PLAN_EXPIRED":
|
|
1902
|
+
case "PLAN_MISMATCH":
|
|
1903
|
+
case "PLAN_STALE":
|
|
1904
|
+
case "UNSUPPORTED_OPERATION":
|
|
1865
1905
|
return 2;
|
|
1866
1906
|
case "AUTH_FAILED":
|
|
1867
1907
|
return 3;
|
|
@@ -1872,8 +1912,15 @@ function agentErrorExitCode(code) {
|
|
|
1872
1912
|
}
|
|
1873
1913
|
}
|
|
1874
1914
|
function classifyAgentError(error) {
|
|
1875
|
-
const
|
|
1915
|
+
const raw = asRecord(error);
|
|
1876
1916
|
const message = errorMessage(error);
|
|
1917
|
+
const detail = raw?.detail;
|
|
1918
|
+
const detailRecord = detail && typeof detail === "object" ? detail : null;
|
|
1919
|
+
const agentCode = raw?.agentCode ?? detailRecord?.agentCode;
|
|
1920
|
+
if (typeof agentCode === "string" && PLAN_AGENT_ERROR_CODES.has(agentCode)) {
|
|
1921
|
+
return { error: { code: agentCode, message } };
|
|
1922
|
+
}
|
|
1923
|
+
const adminError = classifyError(error);
|
|
1877
1924
|
if (isRawNotFound(error) || adminError.code === "not_found") {
|
|
1878
1925
|
return { error: { code: "NOT_FOUND", message } };
|
|
1879
1926
|
}
|
|
@@ -1898,6 +1945,641 @@ function exitWithAgentError(error, options = {}) {
|
|
|
1898
1945
|
process.exit(agentErrorExitCode(envelope.error.code));
|
|
1899
1946
|
}
|
|
1900
1947
|
|
|
1948
|
+
// src/lib/agent-plan-allowlist.ts
|
|
1949
|
+
var AGENT_PLAN_COLLECTIONS = [
|
|
1950
|
+
"articles",
|
|
1951
|
+
"article-categories",
|
|
1952
|
+
"article-tags",
|
|
1953
|
+
"links",
|
|
1954
|
+
"link-categories",
|
|
1955
|
+
"link-tags"
|
|
1956
|
+
];
|
|
1957
|
+
var AGENT_PLAN_COLLECTION_SET = new Set(AGENT_PLAN_COLLECTIONS);
|
|
1958
|
+
function isAgentPlanCollection(collection) {
|
|
1959
|
+
return AGENT_PLAN_COLLECTION_SET.has(collection);
|
|
1960
|
+
}
|
|
1961
|
+
function assertAgentPlanCollection(collection) {
|
|
1962
|
+
if (isAgentPlanCollection(collection)) {
|
|
1963
|
+
return collection;
|
|
1964
|
+
}
|
|
1965
|
+
throw agentPlanError(
|
|
1966
|
+
"UNSUPPORTED_OPERATION",
|
|
1967
|
+
`Collection "${collection}" is not supported for agent plan mutations.`
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
// src/lib/agent-plan-auth.ts
|
|
1972
|
+
import { createHash } from "crypto";
|
|
1973
|
+
function buildAuthContextFingerprint(input) {
|
|
1974
|
+
const apiUrl = input.apiUrl ?? process.env.SOFTWARE_API_URL ?? process.env.NEXT_PUBLIC_SOFTWARE_API_URL ?? "";
|
|
1975
|
+
const tenantId = input.tenantId ?? process.env.SOFTWARE_TENANT_ID ?? "";
|
|
1976
|
+
const secretDigest = createHash("sha256").update(input.secretKey).digest("hex");
|
|
1977
|
+
const material = [input.publishableKey, secretDigest, apiUrl, tenantId].join("\n");
|
|
1978
|
+
return createHash("sha256").update(material).digest("hex");
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// src/lib/agent-plan-store.ts
|
|
1982
|
+
import { mkdir, open, readFile, unlink, writeFile } from "fs/promises";
|
|
1983
|
+
import { homedir as homedir2 } from "os";
|
|
1984
|
+
import { join as join2 } from "path";
|
|
1985
|
+
import { randomUUID } from "crypto";
|
|
1986
|
+
|
|
1987
|
+
// src/lib/agent-plan-hash.ts
|
|
1988
|
+
import { createHash as createHash2 } from "crypto";
|
|
1989
|
+
function sortKeys(value) {
|
|
1990
|
+
if (Array.isArray(value)) {
|
|
1991
|
+
return value.map(sortKeys);
|
|
1992
|
+
}
|
|
1993
|
+
if (value && typeof value === "object") {
|
|
1994
|
+
return Object.keys(value).sort().reduce((acc, key) => {
|
|
1995
|
+
acc[key] = sortKeys(value[key]);
|
|
1996
|
+
return acc;
|
|
1997
|
+
}, {});
|
|
1998
|
+
}
|
|
1999
|
+
return value;
|
|
2000
|
+
}
|
|
2001
|
+
function hashPlanPayload(payload) {
|
|
2002
|
+
const canonical = JSON.stringify(sortKeys(payload));
|
|
2003
|
+
return createHash2("sha256").update(canonical).digest("hex");
|
|
2004
|
+
}
|
|
2005
|
+
function hashAgentPlanEnvelope(input) {
|
|
2006
|
+
return hashPlanPayload({
|
|
2007
|
+
operation: input.operation,
|
|
2008
|
+
collection: input.collection,
|
|
2009
|
+
documentId: input.documentId,
|
|
2010
|
+
payload: input.payload
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// src/lib/agent-plan-id.ts
|
|
2015
|
+
import { resolve as resolve2, sep } from "path";
|
|
2016
|
+
var PLAN_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
2017
|
+
function assertValidPlanId(planId) {
|
|
2018
|
+
if (!PLAN_ID_PATTERN.test(planId)) {
|
|
2019
|
+
throw agentPlanError("INVALID_INPUT", "Invalid plan token.");
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
function safePlanPath(planDir, planId) {
|
|
2023
|
+
assertValidPlanId(planId);
|
|
2024
|
+
const resolvedDir = resolve2(planDir);
|
|
2025
|
+
const resolvedPath = resolve2(resolvedDir, `${planId}.json`);
|
|
2026
|
+
if (resolvedPath !== resolvedDir && !resolvedPath.startsWith(`${resolvedDir}${sep}`)) {
|
|
2027
|
+
throw agentPlanError("INVALID_INPUT", "Invalid plan token.");
|
|
2028
|
+
}
|
|
2029
|
+
return resolvedPath;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// src/lib/agent-plan-store.ts
|
|
2033
|
+
var AGENT_PLAN_TTL_MS = 15 * 60 * 1e3;
|
|
2034
|
+
function resolveAgentPlanDir() {
|
|
2035
|
+
const override = process.env.SOFTWARE_AGENT_PLAN_DIR?.trim();
|
|
2036
|
+
if (override) {
|
|
2037
|
+
return override;
|
|
2038
|
+
}
|
|
2039
|
+
return join2(homedir2(), ".01software", "agent-plans");
|
|
2040
|
+
}
|
|
2041
|
+
function parseExpiresAtMs(expiresAt) {
|
|
2042
|
+
const ms = Date.parse(expiresAt);
|
|
2043
|
+
if (!Number.isFinite(ms)) {
|
|
2044
|
+
throw agentPlanError("PLAN_MISMATCH", "Stored plan has invalid expiresAt.");
|
|
2045
|
+
}
|
|
2046
|
+
return ms;
|
|
2047
|
+
}
|
|
2048
|
+
function parseStoredAgentPlan(raw, planId) {
|
|
2049
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2050
|
+
throw agentPlanError("PLAN_MISMATCH", "Stored plan is corrupted.");
|
|
2051
|
+
}
|
|
2052
|
+
const record = raw;
|
|
2053
|
+
if (record.planId !== planId) {
|
|
2054
|
+
throw agentPlanError("PLAN_MISMATCH", "Stored plan id does not match token.");
|
|
2055
|
+
}
|
|
2056
|
+
if (typeof record.planHash !== "string" || typeof record.operation !== "string" || typeof record.collection !== "string" || typeof record.authFingerprint !== "string" || typeof record.createdAt !== "string" || typeof record.expiresAt !== "string" || typeof record.status !== "string") {
|
|
2057
|
+
throw agentPlanError("PLAN_MISMATCH", "Stored plan is missing required fields.");
|
|
2058
|
+
}
|
|
2059
|
+
if (record.status !== "pending" && record.status !== "claimed" && record.status !== "consumed") {
|
|
2060
|
+
throw agentPlanError("PLAN_MISMATCH", "Stored plan has invalid status.");
|
|
2061
|
+
}
|
|
2062
|
+
if (!isAgentPlanCollection(record.collection)) {
|
|
2063
|
+
throw agentPlanError("PLAN_MISMATCH", "Stored plan has unsupported collection.");
|
|
2064
|
+
}
|
|
2065
|
+
const collection = record.collection;
|
|
2066
|
+
const operation = record.operation;
|
|
2067
|
+
if (operation !== "create" && operation !== "update" && operation !== "delete") {
|
|
2068
|
+
throw agentPlanError("PLAN_MISMATCH", "Stored plan has invalid operation.");
|
|
2069
|
+
}
|
|
2070
|
+
const payload = record.payload == null ? void 0 : typeof record.payload === "object" && !Array.isArray(record.payload) ? record.payload : (() => {
|
|
2071
|
+
throw agentPlanError("PLAN_MISMATCH", "Stored plan payload is invalid.");
|
|
2072
|
+
})();
|
|
2073
|
+
const documentId = record.documentId == null ? void 0 : typeof record.documentId === "string" ? record.documentId : (() => {
|
|
2074
|
+
throw agentPlanError("PLAN_MISMATCH", "Stored plan documentId is invalid.");
|
|
2075
|
+
})();
|
|
2076
|
+
const baseUpdatedAt = record.baseUpdatedAt == null ? void 0 : typeof record.baseUpdatedAt === "string" ? record.baseUpdatedAt : (() => {
|
|
2077
|
+
throw agentPlanError("PLAN_MISMATCH", "Stored plan baseUpdatedAt is invalid.");
|
|
2078
|
+
})();
|
|
2079
|
+
return {
|
|
2080
|
+
planId,
|
|
2081
|
+
planHash: record.planHash,
|
|
2082
|
+
operation,
|
|
2083
|
+
collection,
|
|
2084
|
+
documentId,
|
|
2085
|
+
payload,
|
|
2086
|
+
baseUpdatedAt,
|
|
2087
|
+
authFingerprint: record.authFingerprint,
|
|
2088
|
+
createdAt: record.createdAt,
|
|
2089
|
+
expiresAt: record.expiresAt,
|
|
2090
|
+
status: record.status
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
function assertStoredPlanHashMatches(plan) {
|
|
2094
|
+
const expected = hashAgentPlanEnvelope({
|
|
2095
|
+
operation: plan.operation,
|
|
2096
|
+
collection: plan.collection,
|
|
2097
|
+
documentId: plan.documentId,
|
|
2098
|
+
payload: plan.payload
|
|
2099
|
+
});
|
|
2100
|
+
if (plan.planHash !== expected) {
|
|
2101
|
+
throw agentPlanError("PLAN_MISMATCH", "Stored plan hash does not match plan contents.");
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
function assertPlanNotExpired(plan) {
|
|
2105
|
+
if (Date.now() > parseExpiresAtMs(plan.expiresAt)) {
|
|
2106
|
+
throw agentPlanError("PLAN_EXPIRED", "Plan expired.");
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
async function readAgentPlanFile(planDir, planId) {
|
|
2110
|
+
assertValidPlanId(planId);
|
|
2111
|
+
let raw;
|
|
2112
|
+
try {
|
|
2113
|
+
raw = await readFile(safePlanPath(planDir, planId), "utf8");
|
|
2114
|
+
} catch {
|
|
2115
|
+
throw agentPlanError("PLAN_MISMATCH", "Plan not found.");
|
|
2116
|
+
}
|
|
2117
|
+
let parsed;
|
|
2118
|
+
try {
|
|
2119
|
+
parsed = JSON.parse(raw);
|
|
2120
|
+
} catch {
|
|
2121
|
+
throw agentPlanError("PLAN_MISMATCH", "Stored plan is corrupted.");
|
|
2122
|
+
}
|
|
2123
|
+
return parseStoredAgentPlan(parsed, planId);
|
|
2124
|
+
}
|
|
2125
|
+
async function writeAgentPlanFile(planDir, plan) {
|
|
2126
|
+
await writeFile(safePlanPath(planDir, plan.planId), JSON.stringify(plan), {
|
|
2127
|
+
encoding: "utf8",
|
|
2128
|
+
mode: 384
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
async function writeAgentPlan(input) {
|
|
2132
|
+
await mkdir(input.planDir, { recursive: true, mode: 448 });
|
|
2133
|
+
const planId = input.record.planId ?? randomUUID();
|
|
2134
|
+
assertValidPlanId(planId);
|
|
2135
|
+
const plan = {
|
|
2136
|
+
...input.record,
|
|
2137
|
+
planId,
|
|
2138
|
+
status: "pending"
|
|
2139
|
+
};
|
|
2140
|
+
assertStoredPlanHashMatches(plan);
|
|
2141
|
+
await writeAgentPlanFile(input.planDir, plan);
|
|
2142
|
+
return plan;
|
|
2143
|
+
}
|
|
2144
|
+
async function claimAgentPlan(input) {
|
|
2145
|
+
assertValidPlanId(input.planId);
|
|
2146
|
+
const planPath = safePlanPath(input.planDir, input.planId);
|
|
2147
|
+
const lockPath = `${planPath}.lock`;
|
|
2148
|
+
let lockHandle;
|
|
2149
|
+
try {
|
|
2150
|
+
lockHandle = await open(lockPath, "wx", 384);
|
|
2151
|
+
} catch {
|
|
2152
|
+
throw agentPlanError("PLAN_MISMATCH", "Plan was already claimed or consumed.");
|
|
2153
|
+
}
|
|
2154
|
+
try {
|
|
2155
|
+
const plan = await readAgentPlanFile(input.planDir, input.planId);
|
|
2156
|
+
assertPlanNotExpired(plan);
|
|
2157
|
+
assertStoredPlanHashMatches(plan);
|
|
2158
|
+
if (plan.planHash !== input.planHash) {
|
|
2159
|
+
throw agentPlanError("PLAN_MISMATCH", "Plan hash does not match stored plan.");
|
|
2160
|
+
}
|
|
2161
|
+
if (plan.authFingerprint !== input.authFingerprint) {
|
|
2162
|
+
throw agentPlanError(
|
|
2163
|
+
"PLAN_MISMATCH",
|
|
2164
|
+
"Plan auth context does not match current credentials."
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
if (plan.status !== "pending") {
|
|
2168
|
+
throw agentPlanError("PLAN_MISMATCH", "Plan was already claimed or consumed.");
|
|
2169
|
+
}
|
|
2170
|
+
const claimed = { ...plan, status: "claimed" };
|
|
2171
|
+
await writeAgentPlanFile(input.planDir, claimed);
|
|
2172
|
+
return claimed;
|
|
2173
|
+
} finally {
|
|
2174
|
+
if (lockHandle) {
|
|
2175
|
+
await lockHandle.close().catch(() => {
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
await unlink(lockPath).catch(() => {
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
async function releaseAgentPlanClaim(input) {
|
|
2183
|
+
const plan = await readAgentPlanFile(input.planDir, input.planId);
|
|
2184
|
+
if (plan.status !== "claimed") {
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
await writeAgentPlanFile(input.planDir, { ...plan, status: "pending" });
|
|
2188
|
+
}
|
|
2189
|
+
async function markAgentPlanConsumed(input) {
|
|
2190
|
+
const plan = await readAgentPlanFile(input.planDir, input.planId);
|
|
2191
|
+
await writeAgentPlanFile(input.planDir, { ...plan, status: "consumed" });
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// src/lib/agent-plan-stale.ts
|
|
2195
|
+
function readDocumentUpdatedAt(doc) {
|
|
2196
|
+
if (!doc || typeof doc !== "object") {
|
|
2197
|
+
return void 0;
|
|
2198
|
+
}
|
|
2199
|
+
const updatedAt = doc.updatedAt;
|
|
2200
|
+
return typeof updatedAt === "string" && updatedAt.length > 0 ? updatedAt : void 0;
|
|
2201
|
+
}
|
|
2202
|
+
function assertPlanBaseUpdatedAt(operation, baseUpdatedAt) {
|
|
2203
|
+
if (operation === "create") {
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
if (!baseUpdatedAt) {
|
|
2207
|
+
throw agentPlanError(
|
|
2208
|
+
"INVALID_INPUT",
|
|
2209
|
+
"Document is missing updatedAt; cannot plan update or delete."
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
async function assertPlanNotStale(input) {
|
|
2214
|
+
if (input.operation === "create") {
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
if (!input.baseUpdatedAt || !input.currentUpdatedAt) {
|
|
2218
|
+
throw agentPlanError(
|
|
2219
|
+
"PLAN_STALE",
|
|
2220
|
+
"Document is missing updatedAt for stale check."
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
if (input.baseUpdatedAt !== input.currentUpdatedAt) {
|
|
2224
|
+
throw agentPlanError(
|
|
2225
|
+
"PLAN_STALE",
|
|
2226
|
+
"Document changed since the plan was created."
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// src/lib/agent-plan-confirm.ts
|
|
2232
|
+
async function executeAgentPlanMutation(client, plan) {
|
|
2233
|
+
const collection = client.collections.from(plan.collection);
|
|
2234
|
+
if (plan.operation === "create") {
|
|
2235
|
+
const response = await collection.create(plan.payload ?? {});
|
|
2236
|
+
return response.doc;
|
|
2237
|
+
}
|
|
2238
|
+
if (!plan.documentId) {
|
|
2239
|
+
throw agentPlanError("PLAN_MISMATCH", "Plan is missing document id.");
|
|
2240
|
+
}
|
|
2241
|
+
if (plan.operation === "update") {
|
|
2242
|
+
const current2 = await collection.findById(plan.documentId);
|
|
2243
|
+
await assertPlanNotStale({
|
|
2244
|
+
operation: plan.operation,
|
|
2245
|
+
baseUpdatedAt: plan.baseUpdatedAt,
|
|
2246
|
+
currentUpdatedAt: readDocumentUpdatedAt(current2)
|
|
2247
|
+
});
|
|
2248
|
+
const response = await collection.update(plan.documentId, plan.payload ?? {});
|
|
2249
|
+
return response.doc;
|
|
2250
|
+
}
|
|
2251
|
+
const current = await collection.findById(plan.documentId);
|
|
2252
|
+
await assertPlanNotStale({
|
|
2253
|
+
operation: plan.operation,
|
|
2254
|
+
baseUpdatedAt: plan.baseUpdatedAt,
|
|
2255
|
+
currentUpdatedAt: readDocumentUpdatedAt(current)
|
|
2256
|
+
});
|
|
2257
|
+
return collection.remove(plan.documentId);
|
|
2258
|
+
}
|
|
2259
|
+
async function finalizeAgentPlan(plan) {
|
|
2260
|
+
await markAgentPlanConsumed({
|
|
2261
|
+
planDir: resolveAgentPlanDir(),
|
|
2262
|
+
planId: plan.planId
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// src/lib/agent-plan-payload.ts
|
|
2267
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2268
|
+
import { stdin } from "process";
|
|
2269
|
+
async function readStdinUtf8() {
|
|
2270
|
+
const chunks = [];
|
|
2271
|
+
for await (const chunk of stdin) {
|
|
2272
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2273
|
+
}
|
|
2274
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2275
|
+
}
|
|
2276
|
+
async function readAgentPlanPayload(opts) {
|
|
2277
|
+
const sourceCount = [opts.dataStdin, opts.dataFile].filter(Boolean).length;
|
|
2278
|
+
if (sourceCount !== 1) {
|
|
2279
|
+
throw agentPlanError(
|
|
2280
|
+
"INVALID_INPUT",
|
|
2281
|
+
"Exactly one of --data-stdin or --data-file is required."
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
let raw;
|
|
2285
|
+
try {
|
|
2286
|
+
raw = opts.dataStdin ? await readStdinUtf8() : readFileSync3(opts.dataFile, "utf8");
|
|
2287
|
+
} catch {
|
|
2288
|
+
throw agentPlanError(
|
|
2289
|
+
"INVALID_INPUT",
|
|
2290
|
+
"Mutation data file could not be read."
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
let parsed;
|
|
2294
|
+
try {
|
|
2295
|
+
parsed = JSON.parse(raw);
|
|
2296
|
+
} catch {
|
|
2297
|
+
throw agentPlanError("INVALID_INPUT", "Mutation data must be valid JSON.");
|
|
2298
|
+
}
|
|
2299
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2300
|
+
throw agentPlanError("INVALID_INPUT", "Mutation data must be a JSON object.");
|
|
2301
|
+
}
|
|
2302
|
+
return parsed;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// src/lib/agent-plan-secrets.ts
|
|
2306
|
+
var SECRET_KEY = /(?:^|_)(?:secret|password|token|api[_-]?key|private[_-]?key|credential)(?:$|_)/i;
|
|
2307
|
+
function isSecretFieldName(key) {
|
|
2308
|
+
if (SECRET_KEY.test(key)) {
|
|
2309
|
+
return true;
|
|
2310
|
+
}
|
|
2311
|
+
const snake = key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
2312
|
+
return SECRET_KEY.test(snake);
|
|
2313
|
+
}
|
|
2314
|
+
function assertNoSecretFields(value, path = "") {
|
|
2315
|
+
if (Array.isArray(value)) {
|
|
2316
|
+
value.forEach((item, index) => {
|
|
2317
|
+
assertNoSecretFields(item, `${path}[${index}]`);
|
|
2318
|
+
});
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
if (!value || typeof value !== "object") {
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
for (const [key, child] of Object.entries(value)) {
|
|
2325
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
2326
|
+
if (isSecretFieldName(key)) {
|
|
2327
|
+
throw agentPlanError(
|
|
2328
|
+
"INVALID_INPUT",
|
|
2329
|
+
`Secret-looking field "${fieldPath}" is not allowed in agent mutation payloads.`
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
assertNoSecretFields(child, fieldPath);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// src/lib/agent-plan-token.ts
|
|
2337
|
+
function encodePlanToken(token) {
|
|
2338
|
+
return Buffer.from(JSON.stringify(token), "utf8").toString("base64url");
|
|
2339
|
+
}
|
|
2340
|
+
function decodePlanToken(raw) {
|
|
2341
|
+
try {
|
|
2342
|
+
const parsed = JSON.parse(
|
|
2343
|
+
Buffer.from(raw, "base64url").toString("utf8")
|
|
2344
|
+
);
|
|
2345
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2346
|
+
throw new Error("invalid token");
|
|
2347
|
+
}
|
|
2348
|
+
const record = parsed;
|
|
2349
|
+
if (typeof record.planId !== "string" || typeof record.planHash !== "string" || typeof record.expiresAt !== "string") {
|
|
2350
|
+
throw new Error("invalid token fields");
|
|
2351
|
+
}
|
|
2352
|
+
assertValidPlanId(record.planId);
|
|
2353
|
+
return {
|
|
2354
|
+
planId: record.planId,
|
|
2355
|
+
planHash: record.planHash,
|
|
2356
|
+
expiresAt: record.expiresAt
|
|
2357
|
+
};
|
|
2358
|
+
} catch {
|
|
2359
|
+
throw agentPlanError("INVALID_INPUT", "Invalid plan token.");
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
function buildConfirmCommand(planToken, planHash) {
|
|
2363
|
+
return `01 agent confirm ${planToken} --hash ${planHash}`;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// src/commands/agent-plan.ts
|
|
2367
|
+
function invalidInput(message, field) {
|
|
2368
|
+
const error = new Error(message);
|
|
2369
|
+
Object.assign(error, {
|
|
2370
|
+
type: "validation",
|
|
2371
|
+
code: "invalid_argument",
|
|
2372
|
+
field,
|
|
2373
|
+
detail: { message, field }
|
|
2374
|
+
});
|
|
2375
|
+
return error;
|
|
2376
|
+
}
|
|
2377
|
+
function handleCommanderError(error) {
|
|
2378
|
+
if (error.code === "commander.helpDisplayed") {
|
|
2379
|
+
process.exit(error.exitCode);
|
|
2380
|
+
}
|
|
2381
|
+
exitWithAgentError(invalidInput(error.message, "command"));
|
|
2382
|
+
}
|
|
2383
|
+
function configureAgentParser(command) {
|
|
2384
|
+
command.configureOutput({
|
|
2385
|
+
writeErr: () => {
|
|
2386
|
+
},
|
|
2387
|
+
writeOut: (str) => {
|
|
2388
|
+
process.stdout.write(str);
|
|
2389
|
+
}
|
|
2390
|
+
});
|
|
2391
|
+
command.exitOverride(handleCommanderError);
|
|
2392
|
+
return command;
|
|
2393
|
+
}
|
|
2394
|
+
async function captureBaseUpdatedAt(client, collection, documentId) {
|
|
2395
|
+
const doc = await client.collections.from(collection).findById(documentId);
|
|
2396
|
+
return readDocumentUpdatedAt(doc);
|
|
2397
|
+
}
|
|
2398
|
+
async function runPlanAction(input) {
|
|
2399
|
+
const collection = assertAgentPlanCollection(input.collection);
|
|
2400
|
+
if (input.operation !== "create" && !input.documentId) {
|
|
2401
|
+
throw invalidInput("Missing required argument: id", "id");
|
|
2402
|
+
}
|
|
2403
|
+
let payload;
|
|
2404
|
+
if (input.operation === "create" || input.operation === "update") {
|
|
2405
|
+
payload = await readAgentPlanPayload({
|
|
2406
|
+
dataStdin: input.dataStdin,
|
|
2407
|
+
dataFile: input.dataFile
|
|
2408
|
+
});
|
|
2409
|
+
assertNoSecretFields(payload);
|
|
2410
|
+
}
|
|
2411
|
+
const client = input.getClient();
|
|
2412
|
+
const authFingerprint = buildAuthContextFingerprint({
|
|
2413
|
+
publishableKey: client.publishableKey,
|
|
2414
|
+
secretKey: client.secretKey
|
|
2415
|
+
});
|
|
2416
|
+
let baseUpdatedAt;
|
|
2417
|
+
if (input.operation !== "create" && input.documentId) {
|
|
2418
|
+
baseUpdatedAt = await captureBaseUpdatedAt(
|
|
2419
|
+
client,
|
|
2420
|
+
collection,
|
|
2421
|
+
input.documentId
|
|
2422
|
+
);
|
|
2423
|
+
assertPlanBaseUpdatedAt(input.operation, baseUpdatedAt);
|
|
2424
|
+
}
|
|
2425
|
+
const planHash = hashAgentPlanEnvelope({
|
|
2426
|
+
operation: input.operation,
|
|
2427
|
+
collection,
|
|
2428
|
+
documentId: input.documentId,
|
|
2429
|
+
payload
|
|
2430
|
+
});
|
|
2431
|
+
const createdAt = /* @__PURE__ */ new Date();
|
|
2432
|
+
const expiresAt = new Date(createdAt.getTime() + AGENT_PLAN_TTL_MS);
|
|
2433
|
+
const stored = await writeAgentPlan({
|
|
2434
|
+
planDir: resolveAgentPlanDir(),
|
|
2435
|
+
record: {
|
|
2436
|
+
planHash,
|
|
2437
|
+
operation: input.operation,
|
|
2438
|
+
collection,
|
|
2439
|
+
documentId: input.documentId,
|
|
2440
|
+
payload,
|
|
2441
|
+
baseUpdatedAt,
|
|
2442
|
+
authFingerprint,
|
|
2443
|
+
createdAt: createdAt.toISOString(),
|
|
2444
|
+
expiresAt: expiresAt.toISOString()
|
|
2445
|
+
}
|
|
2446
|
+
});
|
|
2447
|
+
const planToken = encodePlanToken({
|
|
2448
|
+
planId: stored.planId,
|
|
2449
|
+
planHash: stored.planHash,
|
|
2450
|
+
expiresAt: stored.expiresAt
|
|
2451
|
+
});
|
|
2452
|
+
printAgentSuccess(
|
|
2453
|
+
{
|
|
2454
|
+
planToken,
|
|
2455
|
+
planHash: stored.planHash,
|
|
2456
|
+
planId: stored.planId,
|
|
2457
|
+
expiresAt: stored.expiresAt,
|
|
2458
|
+
operation: stored.operation,
|
|
2459
|
+
collection: stored.collection,
|
|
2460
|
+
documentId: stored.documentId,
|
|
2461
|
+
confirmCommand: buildConfirmCommand(planToken, stored.planHash)
|
|
2462
|
+
},
|
|
2463
|
+
{ pretty: Boolean(input.pretty) }
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
function registerAgentPlanCommands(agent, getClient2) {
|
|
2467
|
+
const plan = configureAgentParser(
|
|
2468
|
+
agent.command("plan").description("Plan a mutation without executing")
|
|
2469
|
+
);
|
|
2470
|
+
configureAgentParser(
|
|
2471
|
+
plan.command("create <collection>").description("Plan a document create").option("--data-stdin", "Read mutation JSON from stdin").option("--data-file <path>", "Read mutation JSON from file").option("--pretty", "Print 2-space indented JSON").action(
|
|
2472
|
+
async (collection, opts) => {
|
|
2473
|
+
try {
|
|
2474
|
+
await runPlanAction({
|
|
2475
|
+
operation: "create",
|
|
2476
|
+
collection,
|
|
2477
|
+
dataStdin: opts.dataStdin,
|
|
2478
|
+
dataFile: opts.dataFile,
|
|
2479
|
+
pretty: opts.pretty,
|
|
2480
|
+
getClient: getClient2
|
|
2481
|
+
});
|
|
2482
|
+
} catch (error) {
|
|
2483
|
+
exitWithAgentError(error, { pretty: Boolean(opts.pretty) });
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
)
|
|
2487
|
+
);
|
|
2488
|
+
configureAgentParser(
|
|
2489
|
+
plan.command("update <collection> <id>").description("Plan a document update").option("--data-stdin", "Read mutation JSON from stdin").option("--data-file <path>", "Read mutation JSON from file").option("--pretty", "Print 2-space indented JSON").action(
|
|
2490
|
+
async (collection, id, opts) => {
|
|
2491
|
+
try {
|
|
2492
|
+
await runPlanAction({
|
|
2493
|
+
operation: "update",
|
|
2494
|
+
collection,
|
|
2495
|
+
documentId: id,
|
|
2496
|
+
dataStdin: opts.dataStdin,
|
|
2497
|
+
dataFile: opts.dataFile,
|
|
2498
|
+
pretty: opts.pretty,
|
|
2499
|
+
getClient: getClient2
|
|
2500
|
+
});
|
|
2501
|
+
} catch (error) {
|
|
2502
|
+
exitWithAgentError(error, { pretty: Boolean(opts.pretty) });
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
)
|
|
2506
|
+
);
|
|
2507
|
+
configureAgentParser(
|
|
2508
|
+
plan.command("delete <collection> <id>").description("Plan a document delete").option("--pretty", "Print 2-space indented JSON").action(
|
|
2509
|
+
async (collection, id, opts) => {
|
|
2510
|
+
try {
|
|
2511
|
+
await runPlanAction({
|
|
2512
|
+
operation: "delete",
|
|
2513
|
+
collection,
|
|
2514
|
+
documentId: id,
|
|
2515
|
+
pretty: opts.pretty,
|
|
2516
|
+
getClient: getClient2
|
|
2517
|
+
});
|
|
2518
|
+
} catch (error) {
|
|
2519
|
+
exitWithAgentError(error, { pretty: Boolean(opts.pretty) });
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
)
|
|
2523
|
+
);
|
|
2524
|
+
configureAgentParser(
|
|
2525
|
+
agent.command("confirm <planToken>").description("Execute a previously planned mutation").requiredOption("--hash <planHash>", "Plan hash from plan output").option("--pretty", "Print 2-space indented JSON").action(
|
|
2526
|
+
async (planTokenRaw, opts) => {
|
|
2527
|
+
try {
|
|
2528
|
+
const token = decodePlanToken(planTokenRaw);
|
|
2529
|
+
if (token.planHash !== opts.hash) {
|
|
2530
|
+
throw agentPlanError(
|
|
2531
|
+
"PLAN_MISMATCH",
|
|
2532
|
+
"Plan token does not match --hash."
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
const client = getClient2();
|
|
2536
|
+
const authFingerprint = buildAuthContextFingerprint({
|
|
2537
|
+
publishableKey: client.publishableKey,
|
|
2538
|
+
secretKey: client.secretKey
|
|
2539
|
+
});
|
|
2540
|
+
const planDir = resolveAgentPlanDir();
|
|
2541
|
+
const plan2 = await claimAgentPlan({
|
|
2542
|
+
planDir,
|
|
2543
|
+
planId: token.planId,
|
|
2544
|
+
planHash: opts.hash,
|
|
2545
|
+
authFingerprint
|
|
2546
|
+
});
|
|
2547
|
+
if (token.planId !== plan2.planId) {
|
|
2548
|
+
throw agentPlanError(
|
|
2549
|
+
"PLAN_MISMATCH",
|
|
2550
|
+
"Plan token does not match stored plan."
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2553
|
+
let doc;
|
|
2554
|
+
try {
|
|
2555
|
+
doc = await executeAgentPlanMutation(client, plan2);
|
|
2556
|
+
} catch (mutationError) {
|
|
2557
|
+
await releaseAgentPlanClaim({
|
|
2558
|
+
planDir,
|
|
2559
|
+
planId: plan2.planId
|
|
2560
|
+
}).catch(() => {
|
|
2561
|
+
});
|
|
2562
|
+
throw mutationError;
|
|
2563
|
+
}
|
|
2564
|
+
await finalizeAgentPlan(plan2);
|
|
2565
|
+
printAgentSuccess(
|
|
2566
|
+
{
|
|
2567
|
+
applied: true,
|
|
2568
|
+
operation: plan2.operation,
|
|
2569
|
+
collection: plan2.collection,
|
|
2570
|
+
id: doc && typeof doc === "object" && "id" in doc ? String(doc.id) : plan2.documentId,
|
|
2571
|
+
doc
|
|
2572
|
+
},
|
|
2573
|
+
{ pretty: Boolean(opts.pretty) }
|
|
2574
|
+
);
|
|
2575
|
+
} catch (error) {
|
|
2576
|
+
exitWithAgentError(error, { pretty: Boolean(opts.pretty) });
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
)
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
|
|
1901
2583
|
// src/commands/agent.ts
|
|
1902
2584
|
var AGENT_PROTOCOL_VERSION = "1";
|
|
1903
2585
|
function buildStableAgentFields() {
|
|
@@ -1907,7 +2589,7 @@ function buildStableAgentFields() {
|
|
|
1907
2589
|
updatedAt: { type: "string", queryable: true }
|
|
1908
2590
|
};
|
|
1909
2591
|
}
|
|
1910
|
-
function
|
|
2592
|
+
function invalidInput2(message, field, value) {
|
|
1911
2593
|
const error = new Error(message);
|
|
1912
2594
|
Object.assign(error, {
|
|
1913
2595
|
type: "validation",
|
|
@@ -1920,15 +2602,15 @@ function invalidInput(message, field, value) {
|
|
|
1920
2602
|
function parsePositiveInteger(value, field) {
|
|
1921
2603
|
if (value == null) return void 0;
|
|
1922
2604
|
if (/^[1-9]\d*$/.test(value)) return Number(value);
|
|
1923
|
-
throw
|
|
2605
|
+
throw invalidInput2(`--${field} must be a positive integer`, field, value);
|
|
1924
2606
|
}
|
|
1925
|
-
function
|
|
2607
|
+
function handleCommanderError2(error) {
|
|
1926
2608
|
if (error.code === "commander.helpDisplayed") {
|
|
1927
2609
|
process.exit(error.exitCode);
|
|
1928
2610
|
}
|
|
1929
|
-
exitWithAgentError(
|
|
2611
|
+
exitWithAgentError(invalidInput2(error.message, "command"));
|
|
1930
2612
|
}
|
|
1931
|
-
function
|
|
2613
|
+
function configureAgentParser2(command) {
|
|
1932
2614
|
command.configureOutput({
|
|
1933
2615
|
writeErr: () => {
|
|
1934
2616
|
},
|
|
@@ -1936,7 +2618,7 @@ function configureAgentParser(command) {
|
|
|
1936
2618
|
process.stdout.write(str);
|
|
1937
2619
|
}
|
|
1938
2620
|
});
|
|
1939
|
-
command.exitOverride(
|
|
2621
|
+
command.exitOverride(handleCommanderError2);
|
|
1940
2622
|
return command;
|
|
1941
2623
|
}
|
|
1942
2624
|
function parseWhere(value) {
|
|
@@ -1947,15 +2629,22 @@ function parseWhere(value) {
|
|
|
1947
2629
|
return parsed;
|
|
1948
2630
|
}
|
|
1949
2631
|
} catch {
|
|
1950
|
-
throw
|
|
2632
|
+
throw invalidInput2("--where must be a JSON object", "where", value);
|
|
1951
2633
|
}
|
|
1952
|
-
throw
|
|
2634
|
+
throw invalidInput2("--where must be a JSON object", "where", value);
|
|
1953
2635
|
}
|
|
2636
|
+
var AGENT_READ_OPERATIONS = ["query", "get"];
|
|
2637
|
+
var AGENT_PLAN_OPERATIONS = [
|
|
2638
|
+
"plan:create",
|
|
2639
|
+
"plan:update",
|
|
2640
|
+
"plan:delete"
|
|
2641
|
+
];
|
|
1954
2642
|
function buildAgentManifest() {
|
|
1955
2643
|
const collections = {};
|
|
1956
2644
|
for (const collection of COLLECTIONS3) {
|
|
2645
|
+
const operations = isAgentPlanCollection(collection) ? [...AGENT_READ_OPERATIONS, ...AGENT_PLAN_OPERATIONS] : [...AGENT_READ_OPERATIONS];
|
|
1957
2646
|
collections[collection] = {
|
|
1958
|
-
operations
|
|
2647
|
+
operations,
|
|
1959
2648
|
fields: buildStableAgentFields()
|
|
1960
2649
|
};
|
|
1961
2650
|
}
|
|
@@ -1969,22 +2658,22 @@ function validateAgentCollection(collection) {
|
|
|
1969
2658
|
return validateCollection(collection);
|
|
1970
2659
|
} catch (error) {
|
|
1971
2660
|
const message = error instanceof Error ? error.message : `Unknown collection "${collection}".`;
|
|
1972
|
-
throw
|
|
2661
|
+
throw invalidInput2(message, "collection", collection);
|
|
1973
2662
|
}
|
|
1974
2663
|
}
|
|
1975
2664
|
function registerAgentCommands(program2, getClient2) {
|
|
1976
|
-
const agent =
|
|
2665
|
+
const agent = configureAgentParser2(
|
|
1977
2666
|
program2.command("agent").description("Machine-stable CLI namespace for coding agents").addHelpText(
|
|
1978
2667
|
"after",
|
|
1979
2668
|
"\nContract: docs/agent-cli-contract.md\nJSON only on stdout; no TTY effects."
|
|
1980
2669
|
)
|
|
1981
2670
|
);
|
|
1982
|
-
|
|
2671
|
+
configureAgentParser2(
|
|
1983
2672
|
agent.command("manifest").description("Print the Agent CLI protocol manifest").option("--pretty", "Print 2-space indented JSON").action((opts) => {
|
|
1984
2673
|
printAgentSuccess(buildAgentManifest(), { pretty: Boolean(opts.pretty) });
|
|
1985
2674
|
})
|
|
1986
2675
|
);
|
|
1987
|
-
|
|
2676
|
+
configureAgentParser2(
|
|
1988
2677
|
agent.command("query <collection>").description("Query documents from a collection").option("--where <json>", "Filter conditions as a JSON object").option("--limit <n>", "Max results").option("--page <n>", "Page number").option("--sort <field>", "Sort field; prefix with - for descending").option("--select <fields>", "Comma-separated fields to select").option("--pretty", "Print 2-space indented JSON").action(
|
|
1989
2678
|
async (collection, opts) => {
|
|
1990
2679
|
try {
|
|
@@ -2007,13 +2696,13 @@ function registerAgentCommands(program2, getClient2) {
|
|
|
2007
2696
|
}
|
|
2008
2697
|
)
|
|
2009
2698
|
);
|
|
2010
|
-
|
|
2699
|
+
configureAgentParser2(
|
|
2011
2700
|
agent.command("get <collection> [id]").description("Get a document by ID").option("--select <fields>", "Comma-separated fields to select").option("--pretty", "Print 2-space indented JSON").action(
|
|
2012
2701
|
async (collection, id, opts) => {
|
|
2013
2702
|
try {
|
|
2014
2703
|
const col = validateAgentCollection(collection);
|
|
2015
2704
|
if (!id) {
|
|
2016
|
-
throw
|
|
2705
|
+
throw invalidInput2("Missing required argument: id", "id");
|
|
2017
2706
|
}
|
|
2018
2707
|
const options = {};
|
|
2019
2708
|
if (opts.select) options.select = parseSelect(opts.select);
|
|
@@ -2026,6 +2715,7 @@ function registerAgentCommands(program2, getClient2) {
|
|
|
2026
2715
|
}
|
|
2027
2716
|
)
|
|
2028
2717
|
);
|
|
2718
|
+
registerAgentPlanCommands(agent, getClient2);
|
|
2029
2719
|
}
|
|
2030
2720
|
|
|
2031
2721
|
// src/index.ts
|