@docyrus/docyrus 0.0.26 → 0.0.27

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docyrus/docyrus",
3
- "version": "0.0.26",
3
+ "version": "0.0.27",
4
4
  "private": false,
5
5
  "description": "Docyrus API CLI",
6
6
  "main": "./main.js",
@@ -24,7 +24,7 @@ For detailed specifications of each building block, see [references/core-buildin
24
24
 
25
25
  ### Querying & Data Operations
26
26
 
27
- Unified query engine with column selection, 50+ filter operators, aggregations, formulas, pivots, child queries, and full-text search. Full CRUD with bulk operations.
27
+ Unified query engine with column selection, 50+ filter operators, aggregations, formulas, pivots, child queries, and full-text search. Full CRUD with bulk operations, record comments, and file attachments.
28
28
 
29
29
  See [references/querying-and-data-operations.md](references/querying-and-data-operations.md).
30
30
 
@@ -14,7 +14,7 @@
14
14
  ## CLI
15
15
 
16
16
  - Full-featured CLI (`@docyrus/docyrus`) for terminal and AI agent use
17
- - Commands: environment management, authentication, data operations, schema management (studio), app management, API discovery, AI chat, and direct API requests
17
+ - Commands: environment management, authentication, data operations, record comments, record file attachments, schema management (studio), app management, API discovery, AI chat, and direct API requests
18
18
  - Multi-account, multi-tenant session management
19
19
  - OpenAPI discovery with caching and fallback generation
20
20
  - Interactive TUI mode
@@ -251,6 +251,52 @@ Delete a data source item.
251
251
  | `dataSourceSlug` | string | yes | Data source slug |
252
252
  | `recordId` | string | yes | Record ID |
253
253
 
254
+ ### `docyrus ds comments create <appSlug> <dataSourceSlug> <recordId>`
255
+
256
+ Create a record-scoped comment.
257
+
258
+ | Argument | Type | Required | Description |
259
+ |---|---|---|---|
260
+ | `appSlug` | string | yes | App slug |
261
+ | `dataSourceSlug` | string | yes | Data source slug |
262
+ | `recordId` | string | yes | Record ID |
263
+
264
+ | Option | Type | Description |
265
+ |---|---|---|
266
+ | `--message` | string | Comment message |
267
+ | `--data` | string | Full JSON payload for the comment DTO |
268
+ | `--fromFile` | string | Path to a JSON payload file |
269
+ | `--parentId` | string | Parent comment ID |
270
+ | `--assignedTo` | string | Assigned user ID |
271
+ | `--attachments` | string | JSON attachments payload |
272
+ | `--level` | number | Comment level |
273
+ | `--status` | number | Comment status |
274
+ | `--done` | boolean | Mark comment as done |
275
+
276
+ **Notes:**
277
+ - Use either `--message` or `--data` / `--fromFile`
278
+ - `--data` and `--fromFile` cannot be mixed with field-specific flags
279
+
280
+ ### `docyrus ds files upload <appSlug> <dataSourceSlug> <recordId>`
281
+
282
+ Upload a record-scoped file attachment.
283
+
284
+ | Argument | Type | Required | Description |
285
+ |---|---|---|---|
286
+ | `appSlug` | string | yes | App slug |
287
+ | `dataSourceSlug` | string | yes | Data source slug |
288
+ | `recordId` | string | yes | Record ID |
289
+
290
+ | Option | Type | Description |
291
+ |---|---|---|
292
+ | `--file` | string | Path to the local file to upload |
293
+ | `--contentType` | string | Override the inferred MIME type |
294
+ | `--publicFile` | boolean | Store the file in the public tenant bucket |
295
+
296
+ **Notes:**
297
+ - Uploads use `multipart/form-data`
298
+ - Content type is inferred from the file extension when omitted
299
+
254
300
  ---
255
301
 
256
302
  ## discover — OpenAPI Discovery
@@ -6,6 +6,13 @@
6
6
  - Bulk create, bulk update, bulk delete (batched for performance)
7
7
  - Insert/update/delete with custom return value selection
8
8
 
9
+ ## Record Comments & Files
10
+
11
+ - Record-scoped comments with create, list, fetch by ID, update, and delete operations
12
+ - Comment payloads can include threading (`parentId`), assignee targeting (`assignedTo`), attachments metadata, level, status, and done state
13
+ - Record-scoped file attachments with upload, list, fetch by ID, insert-without-upload, copy/move, and delete operations
14
+ - File uploads support multipart form data, public/private storage selection, and record association
15
+
9
16
  ## Query Engine
10
17
 
11
18
  Every list/get call accepts a structured query payload:
package/server-loader.js CHANGED
@@ -795,6 +795,7 @@ var AgentEnvStore = class {
795
795
 
796
796
  // src/server/agentServer.ts
797
797
  var import_node_child_process = require("node:child_process");
798
+ var import_node_crypto2 = require("node:crypto");
798
799
  var import_promises3 = require("node:fs/promises");
799
800
  var import_node_path3 = require("node:path");
800
801
 
@@ -4397,7 +4398,7 @@ async function walkFiles(params) {
4397
4398
  }
4398
4399
  }
4399
4400
  async function createAgentServer(params) {
4400
- const { port, sessionManager, modelRegistry, authRuntime, context, onCreateSession, onResumeSession } = params;
4401
+ const { port, sessionManager, modelRegistry, authRuntime, context, onCreateSession, onResumeSession, authToken } = params;
4401
4402
  let activeSession = params.session;
4402
4403
  const pendingAskUserRequests = /* @__PURE__ */ new Map();
4403
4404
  const oauthFlowManager = new OAuthFlowManager();
@@ -4424,6 +4425,13 @@ async function createAgentServer(params) {
4424
4425
  });
4425
4426
  }
4426
4427
  app.use("/*", cors({ origin: "*" }));
4428
+ app.use("/*", async (c, next) => {
4429
+ const unauthorizedResponse = authorizeServerRequest(c.req.raw, authToken);
4430
+ if (unauthorizedResponse) {
4431
+ return unauthorizedResponse;
4432
+ }
4433
+ await next();
4434
+ });
4427
4435
  app.get("/api/health", (c) => {
4428
4436
  return c.json({ ok: true });
4429
4437
  });
@@ -5146,6 +5154,140 @@ async function createAgentServer(params) {
5146
5154
  devUrl = null;
5147
5155
  return c.json({ status: "stopped", url: stoppedUrl, pid });
5148
5156
  });
5157
+ function gitExec(args, gitCwd) {
5158
+ return new Promise((res, rej) => {
5159
+ (0, import_node_child_process.execFile)("git", args, { cwd: gitCwd, maxBuffer: 50 * 1024 * 1024 }, (err, stdout) => {
5160
+ if (err) {
5161
+ return rej(err);
5162
+ }
5163
+ res(stdout);
5164
+ });
5165
+ });
5166
+ }
5167
+ app.get("/api/git/diff", async (c) => {
5168
+ const filterPath = c.req.query("path");
5169
+ const cwd = context.cwd;
5170
+ try {
5171
+ let branch;
5172
+ let hasHead = true;
5173
+ try {
5174
+ branch = (await gitExec(["rev-parse", "--abbrev-ref", "HEAD"], cwd)).trim() || void 0;
5175
+ } catch {
5176
+ hasHead = false;
5177
+ }
5178
+ const diffRef = hasHead ? ["diff", "HEAD"] : ["diff", "--cached"];
5179
+ const [nameStatusRaw, untrackedRaw, numstatRaw] = await Promise.all([
5180
+ gitExec([...diffRef, "--name-status"], cwd).catch(() => ""),
5181
+ gitExec(["ls-files", "--others", "--exclude-standard"], cwd).catch(() => ""),
5182
+ gitExec([...diffRef, "--numstat"], cwd).catch(() => "")
5183
+ ]);
5184
+ const entries = [];
5185
+ for (const line of nameStatusRaw.split("\n").filter(Boolean)) {
5186
+ const parts = line.split(" ");
5187
+ const code = parts[0];
5188
+ if (code.startsWith("R")) {
5189
+ entries.push({ path: parts[2], status: "renamed", oldPath: parts[1] });
5190
+ } else if (code.startsWith("C")) {
5191
+ entries.push({ path: parts[2], status: "copied", oldPath: parts[1] });
5192
+ } else if (code.startsWith("M")) {
5193
+ entries.push({ path: parts[1], status: "modified" });
5194
+ } else if (code.startsWith("A")) {
5195
+ entries.push({ path: parts[1], status: "added" });
5196
+ } else if (code.startsWith("D")) {
5197
+ entries.push({ path: parts[1], status: "deleted" });
5198
+ }
5199
+ }
5200
+ for (const filePath of untrackedRaw.split("\n").filter(Boolean)) {
5201
+ entries.push({ path: filePath, status: "untracked" });
5202
+ }
5203
+ const numstatMap = /* @__PURE__ */ new Map();
5204
+ for (const line of numstatRaw.split("\n").filter(Boolean)) {
5205
+ const match2 = line.match(/^(-|\d+)\t(-|\d+)\t(.+)$/);
5206
+ if (!match2) {
5207
+ continue;
5208
+ }
5209
+ const isBinary = match2[1] === "-" && match2[2] === "-";
5210
+ const additions = isBinary ? 0 : Number(match2[1]);
5211
+ const deletions = isBinary ? 0 : Number(match2[2]);
5212
+ let filePath = match2[3];
5213
+ const arrowIdx = filePath.indexOf(" => ");
5214
+ if (arrowIdx !== -1) {
5215
+ const braceStart = filePath.indexOf("{");
5216
+ if (braceStart !== -1 && braceStart < arrowIdx) {
5217
+ const braceEnd = filePath.indexOf("}", arrowIdx);
5218
+ if (braceEnd !== -1) {
5219
+ const prefix = filePath.slice(0, braceStart);
5220
+ const suffix = filePath.slice(braceEnd + 1);
5221
+ const newPart = filePath.slice(braceStart + 1, braceEnd).split(" => ")[1];
5222
+ filePath = prefix + newPart + suffix;
5223
+ }
5224
+ } else {
5225
+ filePath = filePath.slice(arrowIdx + 4);
5226
+ }
5227
+ }
5228
+ numstatMap.set(filePath, { additions, deletions, isBinary });
5229
+ }
5230
+ const filtered = filterPath ? entries.filter((e) => e.path === filterPath) : entries;
5231
+ const MAX_CONTENT_SIZE = 1e6;
5232
+ const fileResults = await Promise.all(filtered.map(async (entry) => {
5233
+ const stats = numstatMap.get(entry.path);
5234
+ if (stats?.isBinary) {
5235
+ return null;
5236
+ }
5237
+ let oldContent = "";
5238
+ let newContent = "";
5239
+ let truncated = false;
5240
+ if (hasHead && (entry.status === "modified" || entry.status === "deleted" || entry.status === "renamed" || entry.status === "copied")) {
5241
+ const showPath = entry.oldPath ?? entry.path;
5242
+ try {
5243
+ oldContent = await gitExec(["show", `HEAD:${showPath}`], cwd);
5244
+ if (oldContent.length > MAX_CONTENT_SIZE) {
5245
+ oldContent = oldContent.slice(0, MAX_CONTENT_SIZE);
5246
+ truncated = true;
5247
+ }
5248
+ } catch {
5249
+ }
5250
+ }
5251
+ if (entry.status !== "deleted") {
5252
+ try {
5253
+ const resolved = (0, import_node_path3.resolve)(cwd, entry.path);
5254
+ const buf = await (0, import_promises3.readFile)(resolved);
5255
+ if (buf.subarray(0, 8192).includes(0)) {
5256
+ return null;
5257
+ }
5258
+ newContent = buf.toString("utf-8");
5259
+ if (newContent.length > MAX_CONTENT_SIZE) {
5260
+ newContent = newContent.slice(0, MAX_CONTENT_SIZE);
5261
+ truncated = true;
5262
+ }
5263
+ } catch {
5264
+ }
5265
+ }
5266
+ const additions = stats?.additions ?? (entry.status === "untracked" ? newContent.split("\n").length : 0);
5267
+ const deletions = stats?.deletions ?? 0;
5268
+ const file = {
5269
+ path: entry.path,
5270
+ status: entry.status,
5271
+ oldContent,
5272
+ newContent,
5273
+ additions,
5274
+ deletions
5275
+ };
5276
+ if (entry.oldPath) {
5277
+ file.oldPath = entry.oldPath;
5278
+ }
5279
+ if (truncated) {
5280
+ file.truncated = true;
5281
+ }
5282
+ return file;
5283
+ }));
5284
+ const files = fileResults.filter((f) => f !== null);
5285
+ return c.json({ files, ...branch ? { branch } : {} });
5286
+ } catch (error) {
5287
+ const message = error instanceof Error ? error.message : String(error);
5288
+ return c.json({ error: message }, 500);
5289
+ }
5290
+ });
5149
5291
  const BOOLEAN_CLI_FLAGS = /* @__PURE__ */ new Set(["json", "verbose", "global", "noAuth", "expand", "i"]);
5150
5292
  function buildCliArgs(pathSegments, query, body) {
5151
5293
  const args = [...pathSegments, "--json"];
@@ -5294,6 +5436,8 @@ async function createAgentServer(params) {
5294
5436
  process.stderr.write(` POST /api/env/serve \u2014 start dev server (pnpm dev)
5295
5437
  `);
5296
5438
  process.stderr.write(` POST /api/env/stop \u2014 stop dev server
5439
+ `);
5440
+ process.stderr.write(` GET /api/git/diff \u2014 uncommitted file diffs
5297
5441
  `);
5298
5442
  process.stderr.write(` * /api/cli/** \u2014 proxy any docyrus CLI command
5299
5443
 
@@ -5326,6 +5470,61 @@ async function createAgentServer(params) {
5326
5470
  }
5327
5471
  }
5328
5472
  }
5473
+ function extractBearerToken(authorizationHeader) {
5474
+ if (!authorizationHeader) {
5475
+ return null;
5476
+ }
5477
+ const trimmed = authorizationHeader.trim();
5478
+ const firstSpaceIndex = trimmed.indexOf(" ");
5479
+ if (firstSpaceIndex <= 0) {
5480
+ return null;
5481
+ }
5482
+ const scheme = trimmed.slice(0, firstSpaceIndex);
5483
+ const token = trimmed.slice(firstSpaceIndex + 1).trim();
5484
+ if (scheme.toLowerCase() !== "bearer" || token.length === 0) {
5485
+ return null;
5486
+ }
5487
+ return token;
5488
+ }
5489
+ function hashToken(token) {
5490
+ return (0, import_node_crypto2.createHash)("sha256").update(token).digest();
5491
+ }
5492
+ function isBearerTokenAuthorized(actualToken, expectedToken) {
5493
+ if (!expectedToken) {
5494
+ return true;
5495
+ }
5496
+ if (!actualToken) {
5497
+ return false;
5498
+ }
5499
+ const actualHash = hashToken(actualToken);
5500
+ const expectedHash = hashToken(expectedToken);
5501
+ return (0, import_node_crypto2.timingSafeEqual)(actualHash, expectedHash);
5502
+ }
5503
+ function createUnauthorizedResponse() {
5504
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
5505
+ status: 401,
5506
+ headers: {
5507
+ "Content-Type": "application/json",
5508
+ "WWW-Authenticate": "Bearer"
5509
+ }
5510
+ });
5511
+ }
5512
+ function authorizeServerRequest(request, expectedToken) {
5513
+ if (!expectedToken || request.method === "OPTIONS") {
5514
+ return null;
5515
+ }
5516
+ const authorizationHeader = request.headers.get("Authorization");
5517
+ const token = extractBearerToken(authorizationHeader);
5518
+ if (!isBearerTokenAuthorized(token, expectedToken)) {
5519
+ return createUnauthorizedResponse();
5520
+ }
5521
+ return null;
5522
+ }
5523
+
5524
+ // src/server/loaderRequest.ts
5525
+ function parseServerLoaderRequest(payload) {
5526
+ return JSON.parse(payload);
5527
+ }
5329
5528
 
5330
5529
  // src/server/server-loader.ts
5331
5530
  function readRequiredEnv(name) {
@@ -5336,7 +5535,7 @@ function readRequiredEnv(name) {
5336
5535
  return value;
5337
5536
  }
5338
5537
  function readLoaderRequest() {
5339
- return JSON.parse(readRequiredEnv("DOCYRUS_PI_REQUEST"));
5538
+ return parseServerLoaderRequest(readRequiredEnv("DOCYRUS_PI_REQUEST"));
5340
5539
  }
5341
5540
  async function loadPiExports() {
5342
5541
  const piPackageDir = readRequiredEnv("PI_PACKAGE_DIR");
@@ -5547,6 +5746,7 @@ Or create ${modelsJsonPath}`
5547
5746
  }
5548
5747
  await createAgentServer({
5549
5748
  session: createServerSessionAdapter({ session, extensionsResult }),
5749
+ authToken: request.auth,
5550
5750
  port: request.port,
5551
5751
  sessionManager: {
5552
5752
  list: () => pi.SessionManager.list(cwd, request.sessionDir),