@bpmsoftwaresolutions/ai-engine-client 1.0.0-beta.0 → 1.0.0-beta.10

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.
Files changed (3) hide show
  1. package/README.md +129 -5
  2. package/package.json +31 -28
  3. package/src/index.js +504 -10
package/README.md CHANGED
@@ -16,7 +16,8 @@ npm install @bpmsoftwaresolutions/ai-engine-client
16
16
  import { AIEngineClient } from '@bpmsoftwaresolutions/ai-engine-client';
17
17
 
18
18
  const client = new AIEngineClient({
19
- baseUrl: 'https://resume-generator-api.politehill-3458aba1.eastus.azurecontainerapps.io'
19
+ baseUrl: 'https://55e8b585-6076-4992-a164-a5398899efdb-00-c56xlmp9cuq9.worf.replit.dev',
20
+ // pass the key via a custom header or interceptor
20
21
  });
21
22
 
22
23
  // Health
@@ -38,13 +39,27 @@ const charter = await client.createProjectCharter({
38
39
 
39
40
  // Read the resulting project roadmap
40
41
  const roadmap = await client.getProjectRoadmap(charter.project_id);
42
+
43
+ const startup = await client.startSessionGovernance({
44
+ objective: 'Persist governed assistant turns for the charter project.',
45
+ allowed_mutation_surfaces: ['src/', 'scripts/'],
46
+ });
47
+
48
+ const persistedTurn = await client.persistAssistantTurn({
49
+ project_id: charter.project_id,
50
+ session_key: startup.session_key,
51
+ user_request: 'Proceed with the next roadmap slice.',
52
+ assistant_summary: 'Completed the implementation step and persisted the governed turn.',
53
+ changed_files: ['src/web/app.py'],
54
+ });
41
55
  ```
42
56
 
43
57
  ### fromEnv
44
58
 
45
59
  ```js
46
60
  const client = AIEngineClient.fromEnv();
47
- // Reads: AI_ENGINE_BASE_URL, AI_ENGINE_API_KEY, AI_ENGINE_ACTOR_ID
61
+ // Reads: AI_ENGINE_BASE_URL, AI_ENGINE_ACCESS_TOKEN, AI_ENGINE_API_KEY,
62
+ // AI_ENGINE_CLIENT_ID, AI_ENGINE_ACTOR_ID
48
63
  ```
49
64
 
50
65
  ---
@@ -54,11 +69,57 @@ const client = AIEngineClient.fromEnv();
54
69
  | Option | Type | Description |
55
70
  |---|---|---|
56
71
  | `baseUrl` | string | **Required.** Base URL of the AI Engine service. |
57
- | `apiKey` | string | Bearer token sent as `Authorization` header. |
72
+ | `accessToken` | string | Static bearer token sent as `Authorization`. |
73
+ | `tokenProvider` | function | Async callback that returns a bearer token string or `{ token }` / `{ accessToken }` per request. |
74
+ | `apiKey` | string | Compatibility API key sent as `X-API-Key` when bearer auth is not configured. |
75
+ | `clientId` | string | Compatibility client id sent as `X-Client-Id` when bearer auth is not configured. |
58
76
  | `actorId` | string | Sent as `X-Actor-Id` for audit trails. Default: `sdk:npm-ai-engine-client`. |
59
77
  | `fetchImpl` | function | Custom fetch (Node 18+ built-in used by default). |
60
78
  | `timeoutMs` | number | Per-request timeout in ms. Default: 30000. |
61
79
 
80
+ ### Token Provider
81
+
82
+ ```js
83
+ const client = new AIEngineClient({
84
+ baseUrl: process.env.AI_ENGINE_BASE_URL,
85
+ tokenProvider: async () => {
86
+ const token = await acquireAccessToken();
87
+ return { token };
88
+ },
89
+ });
90
+ ```
91
+
92
+ ### API Key Compatibility
93
+
94
+ ```js
95
+ const client = new AIEngineClient({
96
+ baseUrl: process.env.AI_ENGINE_BASE_URL,
97
+ clientId: process.env.AI_ENGINE_CLIENT_ID,
98
+ apiKey: process.env.AI_ENGINE_API_KEY,
99
+ });
100
+ ```
101
+
102
+ When `accessToken` or `tokenProvider` is present, the client prefers bearer auth. API key headers are only sent when bearer auth is not configured.
103
+
104
+ ### External Audio Render Example
105
+
106
+ ```js
107
+ const render = await client.createExternalAudioRender({
108
+ text: '# Weekly Update\n\nThe platform migration is on track.',
109
+ voice: 'alloy',
110
+ });
111
+
112
+ const status = await client.getExternalAudioRender(render.audio_render_run_id);
113
+ const audio = await client.downloadExternalAudioRender(render.audio_render_run_id);
114
+ ```
115
+
116
+ ### External Workflow Artifact Example
117
+
118
+ ```js
119
+ const manifest = await client.listExternalWorkflowRunArtifacts('run-123');
120
+ const packet = await client.downloadExternalWorkflowRunArtifact('run-123', 'package_payload');
121
+ ```
122
+
62
123
  ---
63
124
 
64
125
  ## Methods
@@ -67,6 +128,18 @@ const client = AIEngineClient.fromEnv();
67
128
  | Method | Description |
68
129
  |---|---|
69
130
  | `ping()` | Health check + current workflow name/status. |
131
+ | `startSessionGovernance(body)` | Start a governed external session and return the `session_key` required for mutation-capable assistant turns. |
132
+ | `persistAssistantTurn(body)` | Persist an assistant turn to durable SQL through the API and return refreshed status projections. |
133
+ | `createExternalAudioRender({ text, voice, model, speed, file })` | Create a client-scoped external audio render using inline text or multipart upload. |
134
+ | `getExternalAudioRender(audioRenderRunId)` | Read external audio render status for the authenticated client. |
135
+ | `downloadExternalAudioRender(audioRenderRunId)` | Download the rendered MP3 artifact for the authenticated client. |
136
+ | `listExternalWorkflowRunArtifacts(workflowRunId)` | List signed external artifact descriptors for an authenticated external workflow run. |
137
+ | `downloadExternalWorkflowRunArtifact(workflowRunId, artifactType)` | Download one authenticated external workflow-run artifact by type. |
138
+ | `getExternalProjectStatus(projectId)` | API-key-compatible project status read through the external `/api/v1` boundary. |
139
+ | `getExternalProjectRoadmapSummary(projectId)` | API-key-compatible roadmap summary read through the external `/api/v1` boundary. |
140
+ | `getExternalProjectRoadmapActiveItem(projectId)` | API-key-compatible active roadmap item read through the external `/api/v1` boundary. |
141
+ | `listExternalProjectOpenTasks(projectId)` | API-key-compatible open task read through the external `/api/v1` boundary. |
142
+ | `getExternalProjectStatusBundle(projectId)` | One-call API-key-compatible bundle for project status, roadmap summary, active item, and open tasks. |
70
143
 
71
144
  ### Operator Status
72
145
  | Method | Description |
@@ -75,9 +148,24 @@ const client = AIEngineClient.fromEnv();
75
148
  | `currentArchitectureIntegrityStatus()` | Architecture integrity scan results. |
76
149
  | `currentSecurityGovernanceStatus({ environment, topN })` | Security governance findings. |
77
150
  | `currentCodebaseShapeStatus()` | Codebase shape analysis. |
78
- | `getLatestMemoryProjection()` | Current SQL memory projection (startup hydration source). |
151
+ | `getLatestMemoryProjection()` | Current SQL memory projection (startup hydration source) through the general external `/api/v1/latest-memory-projection` route. |
152
+ | `currentProjectStatus({ projectId })` | Current project status projection from SQL memory. |
153
+ | `createDatabaseBackup({ databaseName, outputName, noWait })` | Start a database backup using server-owned Azure backup defaults. |
154
+ | `listDatabaseBackups({ prefix, limit })` | List recent backups from the server-owned backup storage location. |
155
+ | `getDatabaseBackup({ backupId })` | Get one backup artifact by backup id. |
156
+ | `listDatabaseBackupOperations({ databaseName, operationFilter, limit })` | List backup/export operations using server-owned Azure defaults. |
79
157
  | `getDashboard()` | Operator dashboard payload. |
80
158
 
159
+ Preferred backup contract: use `createDatabaseBackup`, `listDatabaseBackups`, `getDatabaseBackup`, and `listDatabaseBackupOperations`. Those routes let the service own Azure storage, SQL server, resource group, subscription, and credential resolution.
160
+
161
+ Low-level compatibility methods:
162
+
163
+ | Method | Description |
164
+ |---|---|
165
+ | `runAzureSqlBacpacBackup({ databaseName, storageAccount, ... })` | Start an Azure SQL BACPAC export through the legacy infra-shaped operator API. |
166
+ | `listAzureSqlBacpacBackups({ storageAccount, container, ... })` | List BACPAC exports through the legacy infra-shaped operator API. |
167
+ | `listAzureSqlBacpacBackupOperations({ databaseName, resourceGroup, serverName, ... })` | List Azure SQL operations through the legacy infra-shaped operator API. |
168
+
81
169
  ### Retrieval Wrapper
82
170
  | Method | Description |
83
171
  |---|---|
@@ -86,6 +174,31 @@ const client = AIEngineClient.fromEnv();
86
174
  | `getSymbolDefinition({ symbolName, qualifiedName, ... })` | Symbol/code definition lookup. |
87
175
  | `getRelatedCode({ symbolKey, qualifiedName, relationshipType, ... })` | Related code lookup. |
88
176
 
177
+ ### Repo Inventory
178
+ | Method | Description |
179
+ |---|---|
180
+ | `listRepositories({ limit })` | List inventoried repositories. |
181
+ | `getRepository(repositoryId)` | Get repository detail. |
182
+ | `listProjects({ repositoryId, repoKey, limit })` | List inventoried projects. |
183
+ | `getProject(projectId)` | Get project detail. |
184
+ | `listCodeFiles({ repositoryId, projectId, language, pathPrefix, page, pageSize })` | Page through inventoried code files. |
185
+ | `getCodeFile(fileId)` | Get file detail. |
186
+ | `getCodeFileContentWindow(fileId, { startLine, endLine })` | Read a bounded file content window from inventory-backed content. |
187
+ | `listCodeSymbolsByFile(fileId, { limit })` | List symbols discovered in a file. |
188
+ | `getCodeSymbol(symbolId, { includeCode, maxLines })` | Get symbol detail. |
189
+ | `searchSymbols({ query, projectScope, maxResults })` | Search inventoried symbols. |
190
+ | `getSymbolRelationships(symbolId, { relationshipType, depth })` | Read symbol relationships from the inventory graph. |
191
+ | `listCodeRelationships({ repositoryId, projectId, relationshipType, limit })` | Read repository/project scoped graph relationships. |
192
+ | `listActionObservations({ repositoryId, projectPath, filePath, symbolId, actionKind, limit })` | Read latest action-grammar observations for a repo scope. |
193
+ | `listCodebaseShapeFindings({ repositoryId, projectPath, filePath, severity, status, limit })` | List latest codebase-shape findings for the selected repo scope. |
194
+ | `listObjectFlowObservations({ repositoryId, projectPath, filePath, objectKind, boundaryKind, limit })` | List latest object-flow observations for the selected repo scope. |
195
+ | `getChangeAnalysis({ fileId, symbolId, limit })` | Aggregate relationships, findings, and structural observations for one file or symbol. |
196
+ | `listRefactorCandidates({ repositoryRoot, limit })` | List governed refactor candidates derived from SQL-backed findings. |
197
+ | `analyzeRefactorCandidate({ filePath, requestedBy, packetId, refactorIntent })` | Build a replayable refactor packet preview for one candidate file. |
198
+ | `getRepoRetrievalPacket(retrievalPacketId)` | Read a retrieval packet through the repo boundary. |
199
+ | `getRepoRetrievalPacketFragments(retrievalPacketId)` | Read the packet fragments for a retrieval packet. |
200
+ | `evaluateProposalScope({ filePath, projectId, changeType, requestedBy, refactorIntent })` | Evaluate governance, scope, and open-task posture for a proposed refactor slice. |
201
+
89
202
  ### Retrieval Management
90
203
  | Method | Description |
91
204
  |---|---|
@@ -129,6 +242,7 @@ const client = AIEngineClient.fromEnv();
129
242
  | `getWorkflowRun(workflowRunId)` | Get run detail. |
130
243
  | `listWorkflowArtifacts(workflowRunId)` | List run artifacts. |
131
244
  | `getWorkflowRunSubstrate(workflowRunId)` | Get run substrate (sessions, turns). |
245
+ | `getWorkflowPlayback(workflowRunId)` | Get the playback-lite workflow timeline view derived from step runs and artifacts. |
132
246
  | `resumeWorkflowRun(workflowRunId, body)` | Resume a paused run. |
133
247
  | `listRecentInspectorRuns({ limit })` | List recent runs via inspector. |
134
248
  | `inspectWorkflowRun(workflowRunId)` | Full inspector view of a run. |
@@ -147,6 +261,11 @@ const client = AIEngineClient.fromEnv();
147
261
  | `createProjectCharter({ projectName, objective, businessContext, successCriteria, priority, constraints, inScope, outOfScope, assumptions, linkedWorkflows, testingStrategy, initialContext, requestedBy })` | Charter a project (writes to durable SQL memory). |
148
262
  | `listProjects({ limit, includeInactive, processStatus, charterStatus })` | List projects. |
149
263
  | `getProject(projectId)` | Get project detail. |
264
+ | `getProjectCharterReport(projectId)` | Get the SQL-backed charter report payload for a project. |
265
+ | `createProjectMarkdownDownload(projectId, { reportType, includeMarkdown })` | Create a markdown download descriptor for `charter` or `implementation_roadmap`. |
266
+ | `downloadProjectMarkdownReport(projectId, reportType)` | Download a rendered markdown report as text plus filename metadata. |
267
+ | `downloadProjectCharterReportMarkdown(projectId)` | Download the charter markdown report. |
268
+ | `getProjectBundle(projectId)` | Get current status, charter report, and roadmap report in one payload. |
150
269
 
151
270
  ### Roadmaps
152
271
  | Method | Description |
@@ -155,6 +274,9 @@ const client = AIEngineClient.fromEnv();
155
274
  | `getProjectRoadmap(projectId)` | Get full roadmap for a project. |
156
275
  | `getProjectRoadmapSummary(projectId)` | Roadmap summary. |
157
276
  | `getProjectRoadmapActiveItem(projectId)` | Current active roadmap item. |
277
+ | `getProjectImplementationRoadmapReport(projectId)` | Get the SQL-backed implementation roadmap report payload. |
278
+ | `downloadProjectImplementationRoadmapReportMarkdown(projectId)` | Download the implementation roadmap markdown report. |
279
+ | `ensureProjectRoadmapTaskSurface(projectId, { requestedBy, assignedTo, createAcceptanceSubtasks })` | Materialize the active roadmap item's parent task and acceptance subtasks. |
158
280
  | `listProjectOpenTasks(projectId)` | Open tasks for a project. |
159
281
  | `getProjectPerformanceMetrics(projectId, { workflowId, workflowRunId, sinceUtc })` | Performance metrics. |
160
282
 
@@ -162,6 +284,8 @@ const client = AIEngineClient.fromEnv();
162
284
  | Method | Description |
163
285
  |---|---|
164
286
  | `createImplementationTask(implementationItemId, { title, implementationPacketId, ... })` | Create a task on a roadmap item. |
287
+
288
+ `createProjectCharter()` now also accepts `linkedWorkflowSlug`, `ensureTaskSurface`, `assignedTo`, and `createAcceptanceSubtasks` so new API-chartered projects can immediately surface an attachable implementation item and durable task tree.
165
289
  | `listImplementationTasks(implementationItemId)` | List tasks. |
166
290
  | `listImplementationSubtasks(taskId)` | List subtasks. |
167
291
  | `updateImplementationTask(taskId, updates)` | Update a task. |
@@ -325,4 +449,4 @@ const client = AIEngineClient.fromEnv();
325
449
 
326
450
  The package talks to the AI Engine web service. All routes listed above are registered unconditionally in the Flask app and served by the deployed Azure Container App.
327
451
 
328
- Operator routes (`/api/operator/*`) use network-trust protection. The external v1 API (`/api/v1/*`) requires `X-Client-Id` and `X-API-Key` headers and is intentionally not wrapped by this client.
452
+ Operator and governed routes expect bearer-token authorization. The external v1 API (`/api/v1/*`) still supports its migration auth modes separately and is intentionally not wrapped by this client.
package/package.json CHANGED
@@ -1,28 +1,31 @@
1
- {
2
- "name": "@bpmsoftwaresolutions/ai-engine-client",
3
- "version": "1.0.0-beta.0",
4
- "description": "Thin npm client for the AI Engine operator and retrieval APIs",
5
- "type": "module",
6
- "main": "./src/index.js",
7
- "exports": {
8
- ".": "./src/index.js"
9
- },
10
- "files": [
11
- "src",
12
- "README.md"
13
- ],
14
- "engines": {
15
- "node": ">=18"
16
- },
17
- "keywords": [
18
- "ai-engine",
19
- "sdk",
20
- "client",
21
- "retrieval",
22
- "workflow"
23
- ],
24
- "license": "UNLICENSED",
25
- "publishConfig": {
26
- "access": "public"
27
- }
28
- }
1
+ {
2
+ "name": "@bpmsoftwaresolutions/ai-engine-client",
3
+ "version": "1.0.0-beta.10",
4
+ "description": "Thin npm client for the AI Engine operator and retrieval APIs",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "scripts": {
8
+ "test": "node --test"
9
+ },
10
+ "exports": {
11
+ ".": "./src/index.js"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "README.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "keywords": [
21
+ "ai-engine",
22
+ "sdk",
23
+ "client",
24
+ "retrieval",
25
+ "workflow"
26
+ ],
27
+ "license": "UNLICENSED",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }
package/src/index.js CHANGED
@@ -20,11 +20,42 @@ async function readJson(response) {
20
20
  return { message: text };
21
21
  }
22
22
 
23
+ function parseContentDispositionFilename(headerValue) {
24
+ const value = String(headerValue || '');
25
+ const quotedMatch = value.match(/filename="([^"]+)"/i);
26
+ if (quotedMatch) return quotedMatch[1];
27
+ const plainMatch = value.match(/filename=([^;]+)/i);
28
+ return plainMatch ? plainMatch[1].trim() : null;
29
+ }
30
+
31
+ function isFormDataBody(value) {
32
+ return typeof FormData !== 'undefined' && value instanceof FormData;
33
+ }
34
+
35
+ function isBinaryBody(value) {
36
+ return (
37
+ value instanceof ArrayBuffer
38
+ || ArrayBuffer.isView(value)
39
+ || (typeof Blob !== 'undefined' && value instanceof Blob)
40
+ || value instanceof URLSearchParams
41
+ || typeof value === 'string'
42
+ );
43
+ }
44
+
45
+ function isJsonBody(value) {
46
+ if (value === undefined || value === null) return false;
47
+ if (isFormDataBody(value) || isBinaryBody(value)) return false;
48
+ return typeof value === 'object';
49
+ }
50
+
23
51
  export class AIEngineClient {
24
- constructor({ baseUrl, apiKey, actorId, fetchImpl, timeoutMs } = {}) {
52
+ constructor({ baseUrl, accessToken, tokenProvider, apiKey, clientId, actorId, fetchImpl, timeoutMs } = {}) {
25
53
  if (!baseUrl) throw new Error('baseUrl is required.');
26
54
  this.baseUrl = trimTrailingSlash(baseUrl);
55
+ this.accessToken = accessToken || null;
56
+ this.tokenProvider = tokenProvider || null;
27
57
  this.apiKey = apiKey || null;
58
+ this.clientId = clientId || null;
28
59
  this.actorId = actorId || 'sdk:npm-ai-engine-client';
29
60
  this.fetchImpl = fetchImpl || globalThis.fetch;
30
61
  this.timeoutMs = timeoutMs || DEFAULT_TIMEOUT_MS;
@@ -36,7 +67,10 @@ export class AIEngineClient {
36
67
  static fromEnv(options = {}) {
37
68
  return new AIEngineClient({
38
69
  baseUrl: options.baseUrl || process.env.AI_ENGINE_BASE_URL || process.env.AI_ENGINE_API_BASE_URL,
70
+ accessToken: options.accessToken || process.env.AI_ENGINE_ACCESS_TOKEN || null,
71
+ tokenProvider: options.tokenProvider || null,
39
72
  apiKey: options.apiKey || process.env.AI_ENGINE_API_KEY || null,
73
+ clientId: options.clientId || process.env.AI_ENGINE_CLIENT_ID || null,
40
74
  actorId: options.actorId || process.env.AI_ENGINE_ACTOR_ID || 'sdk:npm-ai-engine-client',
41
75
  });
42
76
  }
@@ -77,13 +111,189 @@ export class AIEngineClient {
77
111
  }
78
112
 
79
113
  async getLatestMemoryProjection() {
80
- return this._request('/api/operator/latest-memory-projection');
114
+ return this._request('/api/v1/latest-memory-projection');
115
+ }
116
+
117
+ async currentProjectStatus({ projectId } = {}) {
118
+ return this._request('/api/operator/current-project-status', {
119
+ query: { project_id: projectId },
120
+ });
121
+ }
122
+
123
+ async createDatabaseBackup({ databaseName, outputName, noWait } = {}) {
124
+ return this._request('/api/operator/database/backups', {
125
+ method: 'POST',
126
+ body: {
127
+ database_name: databaseName,
128
+ output_name: outputName,
129
+ no_wait: noWait,
130
+ },
131
+ });
132
+ }
133
+
134
+ async listDatabaseBackups({ prefix, limit } = {}) {
135
+ return this._request('/api/operator/database/backups', {
136
+ query: {
137
+ prefix,
138
+ limit,
139
+ },
140
+ });
141
+ }
142
+
143
+ async getDatabaseBackup({ backupId } = {}) {
144
+ return this._request(`/api/operator/database/backups/${encodeURIComponent(backupId)}`);
145
+ }
146
+
147
+ async listDatabaseBackupOperations({ databaseName, operationFilter, limit } = {}) {
148
+ return this._request('/api/operator/database/backups/operations', {
149
+ query: {
150
+ database_name: databaseName,
151
+ operation_filter: operationFilter,
152
+ limit,
153
+ },
154
+ });
155
+ }
156
+
157
+ async runAzureSqlBacpacBackup({
158
+ databaseName,
159
+ storageAccount,
160
+ container,
161
+ resourceGroup,
162
+ serverName,
163
+ outputName,
164
+ adminUser,
165
+ adminPassword,
166
+ subscription,
167
+ storageKey,
168
+ skipContainerCreate,
169
+ noWait,
170
+ } = {}) {
171
+ return this._request('/api/operator/database/backups/azure-sql-bacpac', {
172
+ method: 'POST',
173
+ body: {
174
+ database_name: databaseName,
175
+ storage_account: storageAccount,
176
+ container,
177
+ resource_group: resourceGroup,
178
+ server_name: serverName,
179
+ output_name: outputName,
180
+ admin_user: adminUser,
181
+ admin_password: adminPassword,
182
+ subscription,
183
+ storage_key: storageKey,
184
+ skip_container_create: skipContainerCreate,
185
+ no_wait: noWait,
186
+ },
187
+ });
188
+ }
189
+
190
+ async listAzureSqlBacpacBackups({
191
+ storageAccount,
192
+ container,
193
+ resourceGroup,
194
+ serverName,
195
+ subscription,
196
+ storageKey,
197
+ prefix,
198
+ limit,
199
+ } = {}) {
200
+ return this._request('/api/operator/database/backups/azure-sql-bacpac', {
201
+ query: {
202
+ storage_account: storageAccount,
203
+ container,
204
+ resource_group: resourceGroup,
205
+ server_name: serverName,
206
+ subscription,
207
+ storage_key: storageKey,
208
+ prefix,
209
+ limit,
210
+ },
211
+ });
212
+ }
213
+
214
+ async listAzureSqlBacpacBackupOperations({
215
+ databaseName,
216
+ resourceGroup,
217
+ serverName,
218
+ subscription,
219
+ operationFilter,
220
+ limit,
221
+ } = {}) {
222
+ return this._request('/api/operator/database/backups/azure-sql-bacpac/operations', {
223
+ query: {
224
+ database_name: databaseName,
225
+ resource_group: resourceGroup,
226
+ server_name: serverName,
227
+ subscription,
228
+ operation_filter: operationFilter,
229
+ limit,
230
+ },
231
+ });
81
232
  }
82
233
 
83
234
  async getDashboard() {
84
235
  return this._request('/api/dashboard');
85
236
  }
86
237
 
238
+ async startSessionGovernance(body) {
239
+ return this._request('/api/v1/session-governance/startup', { method: 'POST', body });
240
+ }
241
+
242
+ async persistAssistantTurn(body) {
243
+ return this._request('/api/v1/assistant-turns', { method: 'POST', body });
244
+ }
245
+
246
+ async getExternalProjectStatus(projectId) {
247
+ return this._request(`/api/v1/projects/${projectId}/status`);
248
+ }
249
+
250
+ async getExternalProjectRoadmapSummary(projectId) {
251
+ return this._request(`/api/v1/projects/${projectId}/implementation-roadmap/summary`);
252
+ }
253
+
254
+ async getExternalProjectRoadmapActiveItem(projectId) {
255
+ return this._request(`/api/v1/projects/${projectId}/implementation-roadmap/active-item`);
256
+ }
257
+
258
+ async listExternalProjectOpenTasks(projectId) {
259
+ return this._request(`/api/v1/projects/${projectId}/open-tasks`);
260
+ }
261
+
262
+ async getExternalProjectStatusBundle(projectId) {
263
+ return this._request(`/api/v1/projects/${projectId}/status-bundle`);
264
+ }
265
+
266
+ async createExternalAudioRender({ text, voice, model, speed, file } = {}) {
267
+ if (file) {
268
+ const form = new FormData();
269
+ form.append('file', file);
270
+ if (voice !== undefined) form.append('voice', String(voice));
271
+ if (model !== undefined) form.append('model', String(model));
272
+ if (speed !== undefined) form.append('speed', String(speed));
273
+ return this._request('/api/v1/audio-renders', { method: 'POST', body: form });
274
+ }
275
+ return this._request('/api/v1/audio-renders', {
276
+ method: 'POST',
277
+ body: { text, voice, model, speed },
278
+ });
279
+ }
280
+
281
+ async getExternalAudioRender(audioRenderRunId) {
282
+ return this._request(`/api/v1/audio-renders/${audioRenderRunId}`);
283
+ }
284
+
285
+ async downloadExternalAudioRender(audioRenderRunId) {
286
+ return this._requestBinary(`/api/v1/audio-renders/${audioRenderRunId}/download`);
287
+ }
288
+
289
+ async listExternalWorkflowRunArtifacts(workflowRunId) {
290
+ return this._request(`/api/v1/runs/${workflowRunId}/artifacts`);
291
+ }
292
+
293
+ async downloadExternalWorkflowRunArtifact(workflowRunId, artifactType) {
294
+ return this._requestBinary(`/api/v1/runs/${workflowRunId}/artifacts/${artifactType}/download`);
295
+ }
296
+
87
297
  // ─── Retrieval Wrapper ─────────────────────────────────────────────────────
88
298
 
89
299
  async getCommandCard({ commandKey, alias, intentText, requestedBy } = {}) {
@@ -126,6 +336,133 @@ export class AIEngineClient {
126
336
  });
127
337
  }
128
338
 
339
+ // ─── Repo Inventory ───────────────────────────────────────────────────────
340
+
341
+ async listRepositories({ limit } = {}) {
342
+ return this._request('/api/repo/repositories', {
343
+ query: { limit },
344
+ });
345
+ }
346
+
347
+ async getRepository(repositoryId) {
348
+ return this._request(`/api/repo/repositories/${repositoryId}`);
349
+ }
350
+
351
+ async listProjects({ repositoryId, repoKey, limit } = {}) {
352
+ return this._request('/api/repo/projects', {
353
+ query: { repository_id: repositoryId, repo_key: repoKey, limit },
354
+ });
355
+ }
356
+
357
+ async getProject(projectId) {
358
+ return this._request(`/api/repo/projects/${projectId}`);
359
+ }
360
+
361
+ async listCodeFiles({ repositoryId, projectId, language, pathPrefix, page = 1, pageSize = 50 } = {}) {
362
+ return this._request('/api/repo/files', {
363
+ query: {
364
+ repository_id: repositoryId,
365
+ project_id: projectId,
366
+ language,
367
+ path_prefix: pathPrefix,
368
+ page,
369
+ page_size: pageSize,
370
+ },
371
+ });
372
+ }
373
+
374
+ async getCodeFile(fileId) {
375
+ return this._request(`/api/repo/files/${fileId}`);
376
+ }
377
+
378
+ async getCodeFileContentWindow(fileId, { startLine = 1, endLine } = {}) {
379
+ return this._request(`/api/repo/files/${fileId}/content-window`, {
380
+ query: { start_line: startLine, end_line: endLine ?? startLine },
381
+ });
382
+ }
383
+
384
+ async listCodeSymbolsByFile(fileId, { limit = 500 } = {}) {
385
+ return this._request(`/api/repo/files/${fileId}/symbols`, {
386
+ query: { limit },
387
+ });
388
+ }
389
+
390
+ async getCodeSymbol(symbolId, { includeCode = false, maxLines = 120 } = {}) {
391
+ return this._request(`/api/repo/symbols/${symbolId}`, {
392
+ query: { include_code: includeCode, max_lines: maxLines },
393
+ });
394
+ }
395
+
396
+ async searchSymbols({ query, projectScope, maxResults = 10 } = {}) {
397
+ return this._request('/api/repo/symbols/search', {
398
+ query: { query, project_scope: projectScope, max_results: maxResults },
399
+ });
400
+ }
401
+
402
+ async getSymbolRelationships(symbolId, { relationshipType, depth = 1 } = {}) {
403
+ return this._request(`/api/repo/symbols/${symbolId}/relationships`, {
404
+ query: { relationship_type: relationshipType, depth },
405
+ });
406
+ }
407
+
408
+ async listCodeRelationships({ repositoryId, projectId, relationshipType, limit = 100 } = {}) {
409
+ return this._request('/api/repo/relationships', {
410
+ query: { repository_id: repositoryId, project_id: projectId, relationship_type: relationshipType, limit },
411
+ });
412
+ }
413
+
414
+ async listActionObservations({ repositoryId, projectPath, filePath, symbolId, actionKind, limit = 100 } = {}) {
415
+ return this._request('/api/repo/action-observations', {
416
+ query: { repository_id: repositoryId, project_path: projectPath, file_path: filePath, symbol_id: symbolId, action_kind: actionKind, limit },
417
+ });
418
+ }
419
+
420
+ async listCodebaseShapeFindings({ repositoryId, projectPath, filePath, severity, status, limit = 100 } = {}) {
421
+ return this._request('/api/repo/shape/findings', {
422
+ query: { repository_id: repositoryId, project_path: projectPath, file_path: filePath, severity, status, limit },
423
+ });
424
+ }
425
+
426
+ async listObjectFlowObservations({ repositoryId, projectPath, filePath, objectKind, boundaryKind, limit = 100 } = {}) {
427
+ return this._request('/api/repo/object-flow-observations', {
428
+ query: { repository_id: repositoryId, project_path: projectPath, file_path: filePath, object_kind: objectKind, boundary_kind: boundaryKind, limit },
429
+ });
430
+ }
431
+
432
+ async getChangeAnalysis({ fileId, symbolId, limit = 25 } = {}) {
433
+ return this._request('/api/repo/change-analysis', {
434
+ query: { file_id: fileId, symbol_id: symbolId, limit },
435
+ });
436
+ }
437
+
438
+ async listRefactorCandidates({ repositoryRoot, limit = 10 } = {}) {
439
+ return this._request('/api/repo/refactor-candidates', {
440
+ query: { repository_root: repositoryRoot, limit },
441
+ });
442
+ }
443
+
444
+ async analyzeRefactorCandidate({ filePath, requestedBy, packetId, refactorIntent } = {}) {
445
+ return this._request('/api/repo/refactor-candidate-analysis', {
446
+ method: 'POST',
447
+ body: { file_path: filePath, requested_by: requestedBy, packet_id: packetId, refactor_intent: refactorIntent },
448
+ });
449
+ }
450
+
451
+ async getRepoRetrievalPacket(retrievalPacketId) {
452
+ return this._request(`/api/repo/retrieval/packets/${retrievalPacketId}`);
453
+ }
454
+
455
+ async getRepoRetrievalPacketFragments(retrievalPacketId) {
456
+ return this._request(`/api/repo/retrieval/packets/${retrievalPacketId}/fragments`);
457
+ }
458
+
459
+ async evaluateProposalScope({ filePath, projectId, changeType = 'refactor', requestedBy, refactorIntent } = {}) {
460
+ return this._request('/api/repo/proposal-scope-evaluation', {
461
+ method: 'POST',
462
+ body: { file_path: filePath, project_id: projectId, change_type: changeType, requested_by: requestedBy, refactor_intent: refactorIntent },
463
+ });
464
+ }
465
+
129
466
  // ─── Retrieval Management ──────────────────────────────────────────────────
130
467
 
131
468
  async getRetrievalStatus() {
@@ -253,6 +590,10 @@ export class AIEngineClient {
253
590
  return this._request(`/api/workflow-runs/${workflowRunId}/substrate`);
254
591
  }
255
592
 
593
+ async getWorkflowPlayback(workflowRunId) {
594
+ return this._request(`/api/operator/workflow-runs/${workflowRunId}/playback`);
595
+ }
596
+
256
597
  async resumeWorkflowRun(workflowRunId, body = {}) {
257
598
  return this._request(`/api/workflow-runs/${workflowRunId}/resume`, { method: 'POST', body });
258
599
  }
@@ -298,9 +639,13 @@ export class AIEngineClient {
298
639
  outOfScope = [],
299
640
  assumptions = [],
300
641
  linkedWorkflows = [],
642
+ linkedWorkflowSlug,
301
643
  testingStrategy = {},
302
644
  initialContext = {},
303
645
  requestedBy,
646
+ ensureTaskSurface = true,
647
+ assignedTo,
648
+ createAcceptanceSubtasks = true,
304
649
  } = {}) {
305
650
  return this._request('/api/projects/charter', {
306
651
  method: 'POST',
@@ -315,9 +660,13 @@ export class AIEngineClient {
315
660
  out_of_scope: outOfScope,
316
661
  assumptions,
317
662
  linked_workflows: linkedWorkflows,
663
+ linked_workflow_slug: linkedWorkflowSlug,
318
664
  testing_strategy: testingStrategy,
319
665
  initial_context: initialContext,
320
666
  requested_by: requestedBy,
667
+ ensure_task_surface: ensureTaskSurface,
668
+ assigned_to: assignedTo,
669
+ create_acceptance_subtasks: createAcceptanceSubtasks,
321
670
  },
322
671
  });
323
672
  }
@@ -337,6 +686,29 @@ export class AIEngineClient {
337
686
  return this._request(`/api/operator/projects/${projectId}`);
338
687
  }
339
688
 
689
+ async getProjectCharterReport(projectId) {
690
+ return this._request(`/api/operator/projects/${projectId}/charter/report`);
691
+ }
692
+
693
+ async createProjectMarkdownDownload(projectId, { reportType, includeMarkdown = false } = {}) {
694
+ return this._request(`/api/operator/projects/${projectId}/markdown-report-downloads`, {
695
+ method: 'POST',
696
+ body: { report_type: reportType, include_markdown: includeMarkdown },
697
+ });
698
+ }
699
+
700
+ async downloadProjectMarkdownReport(projectId, reportType) {
701
+ return this._requestText(`/api/operator/projects/${projectId}/markdown-reports/${reportType}/download`);
702
+ }
703
+
704
+ async downloadProjectCharterReportMarkdown(projectId) {
705
+ return this.downloadProjectMarkdownReport(projectId, 'charter');
706
+ }
707
+
708
+ async getProjectBundle(projectId) {
709
+ return this._request(`/api/operator/projects/${projectId}/bundle`);
710
+ }
711
+
340
712
  // ─── Roadmaps ──────────────────────────────────────────────────────────────
341
713
 
342
714
  async listProjectRoadmaps({ includeInactive } = {}) {
@@ -357,6 +729,29 @@ export class AIEngineClient {
357
729
  return this._request(`/api/operator/projects/${projectId}/implementation-roadmap/active-item`);
358
730
  }
359
731
 
732
+ async getProjectImplementationRoadmapReport(projectId) {
733
+ return this._request(`/api/operator/projects/${projectId}/implementation-roadmap/report`);
734
+ }
735
+
736
+ async downloadProjectImplementationRoadmapReportMarkdown(projectId) {
737
+ return this.downloadProjectMarkdownReport(projectId, 'implementation_roadmap');
738
+ }
739
+
740
+ async ensureProjectRoadmapTaskSurface(projectId, {
741
+ requestedBy,
742
+ assignedTo,
743
+ createAcceptanceSubtasks = true,
744
+ } = {}) {
745
+ return this._request(`/api/operator/projects/${projectId}/implementation-roadmap/task-surface`, {
746
+ method: 'POST',
747
+ body: {
748
+ requested_by: requestedBy,
749
+ assigned_to: assignedTo,
750
+ create_acceptance_subtasks: createAcceptanceSubtasks,
751
+ },
752
+ });
753
+ }
754
+
360
755
  async listProjectOpenTasks(projectId) {
361
756
  return this._request(`/api/operator/projects/${projectId}/open-tasks`);
362
757
  }
@@ -913,21 +1308,45 @@ export class AIEngineClient {
913
1308
 
914
1309
  // ─── Core HTTP ─────────────────────────────────────────────────────────────
915
1310
 
1311
+ async _resolveAccessToken() {
1312
+ if (typeof this.tokenProvider === 'function') {
1313
+ const provided = await this.tokenProvider();
1314
+ if (!provided) return null;
1315
+ if (typeof provided === 'string') return provided;
1316
+ if (typeof provided === 'object' && typeof provided.token === 'string') return provided.token;
1317
+ if (typeof provided === 'object' && typeof provided.accessToken === 'string') return provided.accessToken;
1318
+ throw new Error('tokenProvider must return a token string or an object with a token or accessToken field.');
1319
+ }
1320
+ return this.accessToken;
1321
+ }
1322
+
1323
+ async _buildHeaders({ headers, body, accept }) {
1324
+ const token = await this._resolveAccessToken();
1325
+ const resolvedHeaders = {
1326
+ accept: accept || 'application/json',
1327
+ 'x-actor-id': this.actorId,
1328
+ ...(isJsonBody(body) ? { 'content-type': 'application/json' } : {}),
1329
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
1330
+ ...(!token && this.clientId ? { 'x-client-id': this.clientId } : {}),
1331
+ ...(!token && this.apiKey ? { 'x-api-key': this.apiKey } : {}),
1332
+ ...headers,
1333
+ };
1334
+ for (const [key, value] of Object.entries(resolvedHeaders)) {
1335
+ if (value === undefined) delete resolvedHeaders[key];
1336
+ }
1337
+ return resolvedHeaders;
1338
+ }
1339
+
916
1340
  async _request(path, { method = 'GET', query, headers, body } = {}) {
917
1341
  const url = appendQuery(`${this.baseUrl}${path}`, query);
918
1342
  const controller = new AbortController();
919
1343
  const timeoutHandle = setTimeout(() => controller.abort(), this.timeoutMs);
920
1344
  try {
1345
+ const resolvedHeaders = await this._buildHeaders({ headers, body, accept: 'application/json' });
921
1346
  const response = await this.fetchImpl(url, {
922
1347
  method,
923
- headers: {
924
- accept: 'application/json',
925
- 'content-type': body ? 'application/json' : undefined,
926
- 'x-actor-id': this.actorId,
927
- authorization: this.apiKey ? `Bearer ${this.apiKey}` : undefined,
928
- ...headers,
929
- },
930
- body: body ? JSON.stringify(body) : undefined,
1348
+ headers: resolvedHeaders,
1349
+ body: isJsonBody(body) ? JSON.stringify(body) : body,
931
1350
  signal: controller.signal,
932
1351
  });
933
1352
  const payload = await readJson(response);
@@ -942,6 +1361,81 @@ export class AIEngineClient {
942
1361
  clearTimeout(timeoutHandle);
943
1362
  }
944
1363
  }
1364
+
1365
+ async _requestText(path, { method = 'GET', query, headers, body } = {}) {
1366
+ const url = appendQuery(`${this.baseUrl}${path}`, query);
1367
+ const controller = new AbortController();
1368
+ const timeoutHandle = setTimeout(() => controller.abort(), this.timeoutMs);
1369
+ try {
1370
+ const resolvedHeaders = await this._buildHeaders({
1371
+ headers,
1372
+ body,
1373
+ accept: 'text/markdown, text/plain;q=0.9, application/json;q=0.8',
1374
+ });
1375
+ const response = await this.fetchImpl(url, {
1376
+ method,
1377
+ headers: resolvedHeaders,
1378
+ body: isJsonBody(body) ? JSON.stringify(body) : body,
1379
+ signal: controller.signal,
1380
+ });
1381
+ const contentType = response.headers.get('content-type') || '';
1382
+ const contentDisposition = response.headers.get('content-disposition') || '';
1383
+ if (!response.ok) {
1384
+ const payload = contentType.includes('application/json')
1385
+ ? await response.json()
1386
+ : { message: await response.text() };
1387
+ const error = new Error(payload?.message || payload?.error || `Request failed with status ${response.status}.`);
1388
+ error.status = response.status;
1389
+ error.payload = payload;
1390
+ throw error;
1391
+ }
1392
+ return {
1393
+ text: await response.text(),
1394
+ contentType,
1395
+ fileName: parseContentDispositionFilename(contentDisposition),
1396
+ };
1397
+ } finally {
1398
+ clearTimeout(timeoutHandle);
1399
+ }
1400
+ }
1401
+
1402
+ async _requestBinary(path, { method = 'GET', query, headers, body } = {}) {
1403
+ const url = appendQuery(`${this.baseUrl}${path}`, query);
1404
+ const controller = new AbortController();
1405
+ const timeoutHandle = setTimeout(() => controller.abort(), this.timeoutMs);
1406
+ try {
1407
+ const resolvedHeaders = await this._buildHeaders({
1408
+ headers,
1409
+ body,
1410
+ accept: 'audio/mpeg, application/octet-stream;q=0.9, application/json;q=0.8',
1411
+ });
1412
+ const response = await this.fetchImpl(url, {
1413
+ method,
1414
+ headers: resolvedHeaders,
1415
+ body: isJsonBody(body) ? JSON.stringify(body) : body,
1416
+ signal: controller.signal,
1417
+ });
1418
+ const contentType = response.headers.get('content-type') || '';
1419
+ const contentDisposition = response.headers.get('content-disposition') || '';
1420
+ if (!response.ok) {
1421
+ const payload = contentType.includes('application/json')
1422
+ ? await response.json()
1423
+ : { message: await response.text() };
1424
+ const error = new Error(payload?.message || payload?.error || `Request failed with status ${response.status}.`);
1425
+ error.status = response.status;
1426
+ error.payload = payload;
1427
+ throw error;
1428
+ }
1429
+ const arrayBuffer = await response.arrayBuffer();
1430
+ return {
1431
+ arrayBuffer,
1432
+ contentType,
1433
+ fileName: parseContentDispositionFilename(contentDisposition),
1434
+ };
1435
+ } finally {
1436
+ clearTimeout(timeoutHandle);
1437
+ }
1438
+ }
945
1439
  }
946
1440
 
947
1441
  export function createAIEngineClient(options) {