@xano/cli 1.0.4-beta.2 → 1.0.4-beta.4

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.
@@ -1,9 +1,42 @@
1
1
  import { Command } from '@oclif/core';
2
+ export interface Instance {
3
+ display: string;
4
+ id: string;
5
+ name: string;
6
+ origin: string;
7
+ }
8
+ export interface AuthResult {
9
+ branch: null | string;
10
+ credentialsPath: string;
11
+ instance: {
12
+ id: string;
13
+ name: string;
14
+ origin: string;
15
+ };
16
+ profile: string;
17
+ user: {
18
+ email: string;
19
+ id: string;
20
+ name: string;
21
+ };
22
+ workspace: null | {
23
+ id: string;
24
+ name: string;
25
+ };
26
+ }
27
+ /**
28
+ * Match a user-supplied --instance value against the instance list:
29
+ * numeric values match by ID, URL/hostname values match by the instance
30
+ * origin's hostname, anything else matches by name. Exported for tests.
31
+ */
32
+ export declare function matchInstance(instances: Instance[], query: string): Instance | undefined;
2
33
  export default class Auth extends Command {
3
34
  static description: string;
35
+ static enableJsonFlag: boolean;
4
36
  static examples: string[];
5
37
  static flags: {
6
38
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
39
+ code: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
40
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
41
  insecure: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
42
  instance: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -12,7 +45,9 @@ export default class Auth extends Command {
12
45
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
46
  workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
47
  };
15
- run(): Promise<void>;
48
+ run(): Promise<AuthResult>;
49
+ protected toErrorJson(err: unknown): unknown;
50
+ private acquireToken;
16
51
  private getHeaders;
17
52
  private fetchBranches;
18
53
  private fetchInstances;
@@ -22,7 +57,9 @@ export default class Auth extends Command {
22
57
  private readTokenFromStdin;
23
58
  private resolveBranch;
24
59
  private resolveInstance;
60
+ private resolveProfileName;
25
61
  private resolveWorkspace;
62
+ private resolveWorkspaceAndBranch;
26
63
  private saveProfile;
27
64
  private selectBranch;
28
65
  private selectInstance;
@@ -6,9 +6,37 @@ import * as http from 'node:http';
6
6
  import { dirname } from 'node:path';
7
7
  import open from 'open';
8
8
  import { buildUserAgent, resolveCredentialsPath } from '../../base-command.js';
9
+ function originHostname(value) {
10
+ try {
11
+ return new URL(value.includes('://') ? value : `https://${value}`).hostname.toLowerCase();
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ /**
18
+ * Match a user-supplied --instance value against the instance list:
19
+ * numeric values match by ID, URL/hostname values match by the instance
20
+ * origin's hostname, anything else matches by name. Exported for tests.
21
+ */
22
+ export function matchInstance(instances, query) {
23
+ const q = query.trim();
24
+ if (/^\d+$/.test(q)) {
25
+ return instances.find((inst) => inst.id === q);
26
+ }
27
+ if (q.includes('://') || q.includes('.')) {
28
+ const queryHost = originHostname(q);
29
+ const match = queryHost ? instances.find((inst) => originHostname(inst.origin) === queryHost) : undefined;
30
+ if (match) {
31
+ return match;
32
+ }
33
+ }
34
+ return instances.find((inst) => inst.name === q);
35
+ }
9
36
  const AUTH_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
10
37
  export default class Auth extends Command {
11
38
  static description = 'Authenticate with Xano via browser login';
39
+ static enableJsonFlag = true;
12
40
  static examples = [
13
41
  `$ xano auth
14
42
  Opening browser for Xano login...
@@ -23,10 +51,12 @@ Opening browser for Xano login at https://custom.xano.com...`,
23
51
  To authenticate, open the following URL in any browser:
24
52
  https://app.xano.com/login?dest=cli&display=code
25
53
  ? Paste the code shown in your browser: ****`,
26
- `$ xano auth --no-browser --instance my-instance --workspace 5 --branch dev --profile staging
27
- (non-interactive: only the pasted code is prompted for)`,
54
+ `$ xano auth --code "$CODE" --instance https://my-instance.xano.io --workspace 5
55
+ (fully non-interactive: no browser, no prompts; missing --branch/--profile fall back to defaults)`,
28
56
  `$ echo "$CODE" | xano auth --no-browser --instance my-instance --workspace 5 --branch dev --profile staging
29
57
  (fully scripted: the code is read from piped stdin, no prompt at all)`,
58
+ `$ xano auth --code "$CODE" --instance 42 --workspace 5 --json
59
+ (machine-readable: prints the created profile as JSON)`,
30
60
  ];
31
61
  static flags = {
32
62
  branch: Flags.string({
@@ -34,6 +64,11 @@ To authenticate, open the following URL in any browser:
34
64
  description: 'Pre-select a branch by label (skips the branch picker); pass "" to skip and use the live branch',
35
65
  required: false,
36
66
  }),
67
+ code: Flags.string({
68
+ description: 'Login code copied from the browser (implies --no-browser and runs fully non-interactively). Get the code at <origin>/login?dest=cli&display=code',
69
+ env: 'XANO_AUTH_CODE',
70
+ required: false,
71
+ }),
37
72
  config: Flags.string({
38
73
  char: 'c',
39
74
  description: 'Path to credentials file (default: ~/.xano/credentials.yaml)',
@@ -47,7 +82,7 @@ To authenticate, open the following URL in any browser:
47
82
  }),
48
83
  instance: Flags.string({
49
84
  char: 'i',
50
- description: 'Pre-select an instance by name or numeric ID (skips the instance picker)',
85
+ description: 'Pre-select an instance by name, numeric ID, or instance URL/hostname (skips the instance picker)',
51
86
  required: false,
52
87
  }),
53
88
  'no-browser': Flags.boolean({
@@ -76,12 +111,13 @@ To authenticate, open the following URL in any browser:
76
111
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
77
112
  this.warn('TLS certificate verification is disabled (insecure mode)');
78
113
  }
114
+ // A supplied code (flag or env), piped stdin, or --json output means no
115
+ // prompt can ever open: every unanswered picker falls back to its default.
116
+ const nonInteractive = flags.code !== undefined || (flags['no-browser'] && !process.stdin.isTTY) || this.jsonEnabled();
79
117
  try {
80
- // Step 1: Get token via browser auth
118
+ // Step 1: Get token via supplied code or browser auth
81
119
  this.log('Starting authentication flow...');
82
- const token = flags['no-browser']
83
- ? await this.promptForToken(flags.origin)
84
- : await this.startAuthServer(flags.origin);
120
+ const token = await this.acquireToken(flags.code, flags['no-browser'], flags.origin);
85
121
  // Step 2: Validate token and get user info
86
122
  this.log('');
87
123
  this.log('Validating authentication...');
@@ -110,36 +146,15 @@ To authenticate, open the following URL in any browser:
110
146
  if (instances.length === 0) {
111
147
  this.error('No instances found. Please check your account.');
112
148
  }
113
- instance = await this.resolveInstance(instances, flags.instance);
114
- }
115
- // Step 4: Workspace selection
116
- let workspace;
117
- let branch;
118
- this.log('');
119
- this.log('Fetching available workspaces...');
120
- const workspaces = await this.fetchWorkspaces(token, instance.origin);
121
- if (workspaces.length > 0) {
122
- workspace = await this.resolveWorkspace(workspaces, flags.workspace);
123
- if (workspace) {
124
- // Step 5: Branch selection
125
- this.log('');
126
- this.log('Fetching available branches...');
127
- const branches = await this.fetchBranches(token, instance.origin, workspace.id);
128
- branch = await this.resolveBranch(branches, flags.branch);
129
- }
130
- }
131
- else if (flags.workspace) {
132
- this.error(`Workspace '${flags.workspace}' not found: no workspaces are available on this instance.`);
133
- }
134
- if (flags.branch && !workspace) {
135
- this.warn('Ignoring --branch: no workspace selected.');
149
+ instance = await this.resolveInstance(instances, flags.instance, nonInteractive);
136
150
  }
151
+ // Steps 4 + 5: Workspace and branch selection
152
+ const { branch, workspace } = await this.resolveWorkspaceAndBranch(token, instance.origin, flags, nonInteractive);
137
153
  // Step 6: Profile name
138
154
  this.log('');
139
- // An empty --profile value means "use the default name" (same as accepting the prompt's default)
140
- const profileName = flags.profile === undefined ? await this.promptProfileName() : flags.profile.trim() || 'default';
155
+ const profileName = await this.resolveProfileName(flags.profile, nonInteractive);
141
156
  // Step 7: Save profile
142
- await this.saveProfile({
157
+ const credentialsPath = await this.saveProfile({
143
158
  access_token: token,
144
159
  account_origin: flags.origin,
145
160
  branch,
@@ -150,6 +165,17 @@ To authenticate, open the following URL in any browser:
150
165
  }, flags.config);
151
166
  this.log('');
152
167
  this.log(`Profile '${profileName}' created successfully!`);
168
+ const result = {
169
+ branch: branch ?? null,
170
+ credentialsPath,
171
+ instance: { id: instance.id, name: instance.name, origin: instance.origin },
172
+ profile: profileName,
173
+ user: { email: user.email, id: user.id, name: user.name },
174
+ workspace: workspace ? { id: workspace.id, name: workspace.name } : null,
175
+ };
176
+ if (this.jsonEnabled()) {
177
+ this.logJson(result);
178
+ }
153
179
  // Ensure clean exit (the open() call can keep the event loop alive)
154
180
  process.exit(0);
155
181
  }
@@ -159,11 +185,33 @@ To authenticate, open the following URL in any browser:
159
185
  // @inquirer/core, so the thrown class won't match an imported one.
160
186
  if (error?.name === 'ExitPromptError') {
161
187
  this.log('Authentication cancelled.');
162
- return;
188
+ this.exit(0);
163
189
  }
164
190
  throw error;
165
191
  }
166
192
  }
193
+ // oclif's default toErrorJson serializes Error objects to {} (message is a
194
+ // non-enumerable property), leaving --json consumers with no error detail.
195
+ toErrorJson(err) {
196
+ const error = err;
197
+ return {
198
+ error: {
199
+ ...(error.code ? { code: error.code } : {}),
200
+ message: error.message ?? String(err),
201
+ ...(error.suggestions?.length ? { suggestions: error.suggestions } : {}),
202
+ },
203
+ };
204
+ }
205
+ async acquireToken(code, noBrowser, origin) {
206
+ if (code !== undefined) {
207
+ const token = code.trim();
208
+ if (token === '') {
209
+ this.error(`No code provided. Copy it from ${origin}/login?dest=cli&display=code and pass it via --code or XANO_AUTH_CODE.`);
210
+ }
211
+ return token;
212
+ }
213
+ return noBrowser ? this.promptForToken(origin) : this.startAuthServer(origin);
214
+ }
167
215
  getHeaders(accessToken) {
168
216
  return {
169
217
  'User-Agent': buildUserAgent(this.config.version),
@@ -308,7 +356,7 @@ To authenticate, open the following URL in any browser:
308
356
  process.stdin.on('error', () => resolve(data.trim()));
309
357
  });
310
358
  }
311
- async resolveBranch(branches, flagValue) {
359
+ async resolveBranch(branches, flagValue, nonInteractive) {
312
360
  if (flagValue !== undefined) {
313
361
  // An empty value means "skip and use live branch" (same as the picker's skip option)
314
362
  if (flagValue.trim() === '') {
@@ -322,23 +370,39 @@ To authenticate, open the following URL in any browser:
322
370
  this.log(`Using branch: ${match.label}`);
323
371
  return match.id;
324
372
  }
373
+ if (nonInteractive) {
374
+ this.log('Using live branch (non-interactive; pass --branch to select one)');
375
+ return undefined;
376
+ }
325
377
  return branches.length > 1 ? this.selectBranch(branches) : undefined;
326
378
  }
327
- async resolveInstance(instances, flagValue) {
379
+ async resolveInstance(instances, flagValue, nonInteractive) {
328
380
  if (flagValue) {
329
- // Numeric values match by instance ID, anything else matches by name
330
- const match = /^\d+$/.test(flagValue)
331
- ? instances.find((inst) => inst.id === flagValue)
332
- : instances.find((inst) => inst.name === flagValue);
381
+ const match = matchInstance(instances, flagValue);
333
382
  if (!match) {
334
- this.error(`Instance '${flagValue}' not found. Available instances: ${instances.map((inst) => `${inst.name} (${inst.id})`).join(', ')}`);
383
+ this.error(`Instance '${flagValue}' not found (match by name, numeric ID, or instance URL). Available instances: ${instances.map((inst) => `${inst.name} (${inst.id})`).join(', ')}`);
335
384
  }
336
385
  this.log(`Using instance: ${match.name} (${match.display})`);
337
386
  return match;
338
387
  }
388
+ if (nonInteractive) {
389
+ if (instances.length === 1) {
390
+ this.log(`Using instance: ${instances[0].name} (${instances[0].display})`);
391
+ return instances[0];
392
+ }
393
+ this.error(`Multiple instances available; pass --instance to select one. Available instances: ${instances.map((inst) => `${inst.name} (${inst.id})`).join(', ')}`);
394
+ }
339
395
  return this.selectInstance(instances);
340
396
  }
341
- async resolveWorkspace(workspaces, flagValue) {
397
+ async resolveProfileName(flagValue, nonInteractive) {
398
+ // An empty --profile value means "use the default name" (same as accepting the prompt's default);
399
+ // non-interactive runs with no --profile fall back to the default name too.
400
+ if (flagValue !== undefined) {
401
+ return flagValue.trim() || 'default';
402
+ }
403
+ return nonInteractive ? 'default' : this.promptProfileName();
404
+ }
405
+ async resolveWorkspace(workspaces, flagValue, nonInteractive) {
342
406
  if (flagValue !== undefined) {
343
407
  // An empty value means "skip workspace" (same as the picker's skip option)
344
408
  if (flagValue.trim() === '') {
@@ -352,8 +416,35 @@ To authenticate, open the following URL in any browser:
352
416
  this.log(`Using workspace: ${match.name} (${match.id})`);
353
417
  return match;
354
418
  }
419
+ if (nonInteractive) {
420
+ this.log('Skipping workspace selection (non-interactive; pass --workspace to select one)');
421
+ return undefined;
422
+ }
355
423
  return this.selectWorkspace(workspaces);
356
424
  }
425
+ async resolveWorkspaceAndBranch(token, instanceOrigin, flags, nonInteractive) {
426
+ let workspace;
427
+ let branch;
428
+ this.log('');
429
+ this.log('Fetching available workspaces...');
430
+ const workspaces = await this.fetchWorkspaces(token, instanceOrigin);
431
+ if (workspaces.length > 0) {
432
+ workspace = await this.resolveWorkspace(workspaces, flags.workspace, nonInteractive);
433
+ if (workspace) {
434
+ this.log('');
435
+ this.log('Fetching available branches...');
436
+ const branches = await this.fetchBranches(token, instanceOrigin, workspace.id);
437
+ branch = await this.resolveBranch(branches, flags.branch, nonInteractive);
438
+ }
439
+ }
440
+ else if (flags.workspace) {
441
+ this.error(`Workspace '${flags.workspace}' not found: no workspaces are available on this instance.`);
442
+ }
443
+ if (flags.branch && !workspace) {
444
+ this.warn('Ignoring --branch: no workspace selected.');
445
+ }
446
+ return { branch, workspace };
447
+ }
357
448
  async saveProfile(profile, configPath) {
358
449
  const credentialsPath = resolveCredentialsPath(configPath);
359
450
  const credDir = dirname(credentialsPath);
@@ -393,6 +484,7 @@ To authenticate, open the following URL in any browser:
393
484
  noRefs: true,
394
485
  });
395
486
  fs.writeFileSync(credentialsPath, yamlContent, 'utf8');
487
+ return credentialsPath;
396
488
  }
397
489
  async selectBranch(branches) {
398
490
  const { selectedBranch } = await inquirer.prompt([
@@ -2,6 +2,7 @@ import { Flags } from '@oclif/core';
2
2
  import snakeCase from 'lodash.snakecase';
3
3
  import BaseCommand from '../../../base-command.js';
4
4
  import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
5
+ import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
5
6
  import * as fs from 'node:fs';
6
7
  import * as path from 'node:path';
7
8
  export default class SandboxPull extends BaseCommand {
@@ -87,7 +88,6 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
87
88
  }
88
89
  if (documents.length === 0) {
89
90
  this.log('No documents found in response');
90
- return;
91
91
  }
92
92
  const outputDir = path.resolve(flags.directory);
93
93
  fs.mkdirSync(outputDir, { recursive: true });
@@ -182,7 +182,17 @@ Pulled 42 documents from sandbox environment to ./my-sandbox
182
182
  fs.writeFileSync(filePath, doc.content, 'utf8');
183
183
  writtenCount++;
184
184
  }
185
- this.log(`Pulled ${writtenCount} documents from sandbox environment to ${flags.directory}`);
185
+ // ── Pull knowledge ────────────────────────────────────────────────────
186
+ const knowledgeUrl = `${profile.instance_origin}/api:meta/sandbox/knowledge/sync`;
187
+ const knowledgeObjects = await fetchKnowledge(knowledgeUrl, '', profile.access_token, this.verboseFetch.bind(this), flags.verbose);
188
+ let knowledgeCount = 0;
189
+ if (knowledgeObjects.length > 0) {
190
+ knowledgeCount = writeKnowledge(knowledgeObjects, outputDir);
191
+ }
192
+ const parts = [`${writtenCount} documents`];
193
+ if (knowledgeCount > 0)
194
+ parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
195
+ this.log(`Pulled ${parts.join(' + ')} from sandbox environment to ${flags.directory}`);
186
196
  }
187
197
  sanitizeFilename(name) {
188
198
  return snakeCase(name.replaceAll('"', ''));
@@ -19,6 +19,6 @@ export default class SandboxPush extends BaseCommand {
19
19
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
20
  };
21
21
  run(): Promise<void>;
22
- private openReview;
23
22
  private getFrontendUrl;
23
+ private openReview;
24
24
  }
@@ -130,12 +130,29 @@ Push and open sandbox review in the browser
130
130
  branch: '',
131
131
  command: this,
132
132
  inputDir,
133
+ knowledge: {
134
+ listUrl: () => `${baseUrl}/knowledge/sync`,
135
+ rootDir: inputDir,
136
+ },
133
137
  verboseFetch: this.verboseFetch.bind(this),
134
138
  }, target, pushFlags);
135
139
  if (flags.review) {
136
140
  await this.openReview(profile.instance_origin, profile.access_token, flags.verbose);
137
141
  }
138
142
  }
143
+ getFrontendUrl(instanceOrigin) {
144
+ try {
145
+ const url = new URL(instanceOrigin);
146
+ if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
147
+ url.port = '4200';
148
+ return url.origin;
149
+ }
150
+ }
151
+ catch {
152
+ // fall through
153
+ }
154
+ return instanceOrigin;
155
+ }
139
156
  async openReview(instanceOrigin, accessToken, verbose) {
140
157
  const response = await this.verboseFetch(`${instanceOrigin}/api:meta/sandbox/impersonate`, {
141
158
  headers: {
@@ -158,17 +175,4 @@ Push and open sandbox review in the browser
158
175
  this.log('Opening sandbox review...');
159
176
  await open(reviewUrl);
160
177
  }
161
- getFrontendUrl(instanceOrigin) {
162
- try {
163
- const url = new URL(instanceOrigin);
164
- if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
165
- url.port = '4200';
166
- return url.origin;
167
- }
168
- }
169
- catch {
170
- // fall through
171
- }
172
- return instanceOrigin;
173
- }
174
178
  }
@@ -4,11 +4,12 @@ import * as path from 'node:path';
4
4
  import snakeCase from 'lodash.snakecase';
5
5
  import BaseCommand from '../../../base-command.js';
6
6
  import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
7
+ import { fetchKnowledge, writeKnowledge } from '../../../utils/knowledge-sync.js';
7
8
  export default class Pull extends BaseCommand {
8
9
  static description = 'Pull a workspace multidoc from the Xano Metadata API and split into individual files';
9
10
  static examples = [
10
11
  `$ xano workspace pull
11
- Pulled 42 documents to current directory
12
+ Pulled 42 documents + 5 knowledge files to current directory
12
13
  `,
13
14
  `$ xano workspace pull -d ./my-workspace
14
15
  Pulled 42 documents to ./my-workspace
@@ -239,7 +240,17 @@ Pulled 58 documents
239
240
  fs.writeFileSync(filePath, doc.content, 'utf8');
240
241
  writtenCount++;
241
242
  }
242
- this.log(`Pulled ${writtenCount} documents to ${flags.directory}`);
243
+ // ── Pull knowledge ────────────────────────────────────────────────────
244
+ const knowledgeUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/knowledge/sync`;
245
+ const knowledgeObjects = await fetchKnowledge(knowledgeUrl, branch, profile.access_token, this.verboseFetch.bind(this), flags.verbose);
246
+ let knowledgeCount = 0;
247
+ if (knowledgeObjects.length > 0) {
248
+ knowledgeCount = writeKnowledge(knowledgeObjects, outputDir);
249
+ }
250
+ const parts = [`${writtenCount} documents`];
251
+ if (knowledgeCount > 0)
252
+ parts.push(`${knowledgeCount} knowledge file${knowledgeCount === 1 ? '' : 's'}`);
253
+ this.log(`Pulled ${parts.join(' + ')} to ${flags.directory}`);
243
254
  }
244
255
  /**
245
256
  * Sanitize a document name for use as a filename.
@@ -4,8 +4,8 @@ export default class Push extends BaseCommand {
4
4
  static examples: string[];
5
5
  static flags: {
6
6
  branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
- directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
7
  delete: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ directory: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
9
  'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
10
  env: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
11
  exclude: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
@@ -53,6 +53,12 @@ Push all files except tables
53
53
  `,
54
54
  `$ xano workspace push -i "function/*" -e "**/test*"
55
55
  Push functions but exclude test files
56
+ `,
57
+ `$ xano workspace push -i "knowledge/**"
58
+ Push only knowledge files (agents.md / skills / docs)
59
+ `,
60
+ `$ xano workspace push --sync --delete
61
+ Full sync including knowledge files; removes server objects not present locally
56
62
  `,
57
63
  ];
58
64
  static flags = {
@@ -62,17 +68,17 @@ Push functions but exclude test files
62
68
  description: 'Branch name (optional if set in profile, defaults to live)',
63
69
  required: false,
64
70
  }),
71
+ delete: Flags.boolean({
72
+ default: false,
73
+ description: '[CRITICAL] STOP and confirm with the user before running. Delete workspace objects not included in the push (requires --sync).',
74
+ required: false,
75
+ }),
65
76
  directory: Flags.string({
66
77
  char: 'd',
67
78
  default: '.',
68
79
  description: 'Directory containing documents to push (defaults to current directory)',
69
80
  required: false,
70
81
  }),
71
- delete: Flags.boolean({
72
- default: false,
73
- description: '[CRITICAL] STOP and confirm with the user before running. Delete workspace objects not included in the push (requires --sync).',
74
- required: false,
75
- }),
76
82
  'dry-run': Flags.boolean({
77
83
  default: false,
78
84
  description: 'Show preview of changes without pushing (exit after preview)',
@@ -186,6 +192,10 @@ Push functions but exclude test files
186
192
  branch,
187
193
  command: this,
188
194
  inputDir,
195
+ knowledge: {
196
+ listUrl: () => `${baseUrl}/knowledge/sync`,
197
+ rootDir: inputDir,
198
+ },
189
199
  verboseFetch: this.verboseFetch.bind(this),
190
200
  }, target, pushFlags);
191
201
  }
@@ -0,0 +1,113 @@
1
+ export declare const KNOWLEDGE_DIR = "knowledge";
2
+ export type KnowledgeType = 'agents.md' | 'doc' | 'skill';
3
+ /** A reference file attached to a skill (knowledge_file). */
4
+ export interface KnowledgeFile {
5
+ content: string;
6
+ /** Path relative to the skill's `references/` folder, POSIX-separated. */
7
+ name: string;
8
+ }
9
+ /** A workspace-scoped knowledge object as exchanged with the API. */
10
+ export interface KnowledgeObject {
11
+ content: string;
12
+ description?: string;
13
+ enabled?: boolean;
14
+ files?: KnowledgeFile[];
15
+ guid?: string;
16
+ knowledge_type: KnowledgeType;
17
+ mode?: string;
18
+ name: string;
19
+ scope?: string;
20
+ }
21
+ /** A locally-collected object that also remembers its primary file on disk (for GUID writeback). */
22
+ export interface LocalKnowledgeObject extends KnowledgeObject {
23
+ filePath: string;
24
+ }
25
+ export interface KnowledgePushBody {
26
+ branch?: string;
27
+ delete?: boolean;
28
+ dry_run?: boolean;
29
+ force?: boolean;
30
+ items: KnowledgeObject[];
31
+ }
32
+ export interface KnowledgePushResult {
33
+ deleted?: number;
34
+ guid_map?: Array<{
35
+ guid: string;
36
+ name: string;
37
+ }>;
38
+ imported?: number;
39
+ operations?: KnowledgeDryRunOperation[];
40
+ summary?: Record<string, KnowledgeDryRunSummary>;
41
+ }
42
+ export interface KnowledgeDryRunOperation {
43
+ action: string;
44
+ name: string;
45
+ type: string;
46
+ }
47
+ export interface KnowledgeDryRunSummary {
48
+ created: number;
49
+ deleted: number;
50
+ unchanged: number;
51
+ updated: number;
52
+ }
53
+ export interface KnowledgeDryRunResult {
54
+ operations: KnowledgeDryRunOperation[];
55
+ summary: Record<string, KnowledgeDryRunSummary>;
56
+ }
57
+ type VerboseFetch = (url: string, options: RequestInit, verbose: boolean, authToken?: string) => Promise<Response>;
58
+ /**
59
+ * Build a primary `.md` file body: YAML frontmatter (built from the object's
60
+ * structured fields) followed by the markdown content.
61
+ */
62
+ export declare function buildPrimaryContent(obj: KnowledgeObject): string;
63
+ /**
64
+ * Split a primary `.md` file into its frontmatter fields and markdown body.
65
+ * Returns `{meta: {}, body: <raw>}` when there is no leading `---` block.
66
+ */
67
+ export declare function parsePrimaryContent(raw: string): {
68
+ body: string;
69
+ meta: Record<string, unknown>;
70
+ };
71
+ /** Read just `guid`/`name` from frontmatter (used by GUID matching). */
72
+ export declare function parseFrontmatter(content: string): {
73
+ guid?: string;
74
+ name?: string;
75
+ };
76
+ /**
77
+ * Write knowledge objects under `<outputDir>/knowledge/`, building frontmatter
78
+ * and laying out skill reference files under `<skill>/references/`.
79
+ * Returns the number of files written (primaries + references).
80
+ */
81
+ export declare function writeKnowledge(objects: KnowledgeObject[], outputDir: string): number;
82
+ /**
83
+ * Walk `<inputDir>/knowledge/`, reconstructing structured knowledge objects from
84
+ * primary `.md` files (frontmatter → fields, body → content) and attaching each
85
+ * skill's `references/` files. Include/exclude globs are matched against
86
+ * `knowledge/<path>` (relative to inputDir), consistent with multidoc filtering.
87
+ * Returns `[]` when the directory is absent.
88
+ */
89
+ export declare function collectKnowledgeObjects(inputDir: string, include?: string[], exclude?: string[]): LocalKnowledgeObject[];
90
+ /** Strip the local-only `filePath` before sending objects to the API. */
91
+ export declare function toPushItems(objects: LocalKnowledgeObject[]): KnowledgeObject[];
92
+ /**
93
+ * GET workspace knowledge objects (with content). Returns `[]` on 404 (instance
94
+ * without the feature) or any network error, so pull degrades gracefully.
95
+ */
96
+ export declare function fetchKnowledge(baseUrl: string, branch: string, accessToken: string, verboseFetch: VerboseFetch, verbose: boolean): Promise<KnowledgeObject[]>;
97
+ /**
98
+ * POST knowledge objects to the workspace. Throws on non-2xx. When `body.dry_run`
99
+ * is set the result may carry `operations`/`summary` instead of `imported`.
100
+ */
101
+ export declare function pushKnowledge(url: string, accessToken: string, verboseFetch: VerboseFetch, verbose: boolean, body: KnowledgePushBody): Promise<KnowledgePushResult>;
102
+ /**
103
+ * Compute a dry-run-style preview by diffing local objects against server
104
+ * objects (match by `guid`, then `name`). Used when the server doesn't honor
105
+ * the `dry_run` flag.
106
+ */
107
+ export declare function knowledgePreview(local: KnowledgeObject[], server: KnowledgeObject[], willDelete: boolean): KnowledgeDryRunResult;
108
+ /**
109
+ * Set or update `guid:` in a primary `.md` file's YAML frontmatter.
110
+ * Returns true if the file was modified.
111
+ */
112
+ export declare function syncGuidToFrontmatter(filePath: string, guid: string): boolean;
113
+ export {};