@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 CHANGED
@@ -129,27 +129,32 @@ ${entry}
129
129
  // src/lib/output.ts
130
130
  import pc from "picocolors";
131
131
 
132
- // src/lib/admin-error.ts
133
- var PERMISSION_CODES = [
134
- "tenant_mismatch",
135
- "account_suspended",
136
- "feature_disabled",
137
- "role_denied",
138
- "credential_invalid",
139
- "pat_tenant_unpinned",
140
- "publishable_key_mismatch",
141
- "scope_denied"
142
- ];
143
- var DEGRADED_CODES = [
144
- "redis_unavailable",
145
- "provider_unavailable",
146
- "rate_limited"
147
- ];
148
- var NETWORK_CODES = [
149
- "upstream_timeout",
150
- "upstream_5xx",
151
- "dns_failure"
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 classifyError(err) {
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((resolve2, reject) => {
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
- resolve2({ port: addr.port, cleanup });
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 adminError = classifyError(error);
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 invalidInput(message, field, value) {
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 invalidInput(`--${field} must be a positive integer`, field, value);
2605
+ throw invalidInput2(`--${field} must be a positive integer`, field, value);
1924
2606
  }
1925
- function handleCommanderError(error) {
2607
+ function handleCommanderError2(error) {
1926
2608
  if (error.code === "commander.helpDisplayed") {
1927
2609
  process.exit(error.exitCode);
1928
2610
  }
1929
- exitWithAgentError(invalidInput(error.message, "command"));
2611
+ exitWithAgentError(invalidInput2(error.message, "command"));
1930
2612
  }
1931
- function configureAgentParser(command) {
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(handleCommanderError);
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 invalidInput("--where must be a JSON object", "where", value);
2632
+ throw invalidInput2("--where must be a JSON object", "where", value);
1951
2633
  }
1952
- throw invalidInput("--where must be a JSON object", "where", value);
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: ["query", "get"],
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 invalidInput(message, "collection", collection);
2661
+ throw invalidInput2(message, "collection", collection);
1973
2662
  }
1974
2663
  }
1975
2664
  function registerAgentCommands(program2, getClient2) {
1976
- const agent = configureAgentParser(
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
- configureAgentParser(
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
- configureAgentParser(
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
- configureAgentParser(
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 invalidInput("Missing required argument: id", "id");
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