@amaster.ai/pi-teamwork 0.1.2 → 0.1.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.
package/dist/index.js CHANGED
@@ -1,8 +1,23 @@
1
+ import { spawn } from 'node:child_process';
1
2
  import { loadPiSettings } from '@amaster.ai/pi-shared/settings';
3
+ import { defineTool } from '@earendil-works/pi-coding-agent';
2
4
  import { Type } from 'typebox';
5
+ import { initAmasterProvider } from './adapters/amaster.js';
3
6
  import { initMulticaProvider } from './adapters/multica.js';
4
7
  const SETTINGS_KEY = 'pi-teamwork';
5
8
  const STATUS_KEY = 'pi-teamwork';
9
+ const TEAMWORK_TOOL_ORDER = [
10
+ 'workspace_list',
11
+ 'issue_list',
12
+ 'issue_get',
13
+ 'issue_create',
14
+ 'issue_update',
15
+ 'issue_comment',
16
+ 'project_list',
17
+ 'user_directory_list',
18
+ 'teamwork_status',
19
+ ];
20
+ const TEAMWORK_TOOL_NAMES = new Set(TEAMWORK_TOOL_ORDER);
6
21
  const TEAMWORK_GUIDANCE = [
7
22
  '<teamwork-guidance>',
8
23
  'You are working in a shared project tracker (teamwork) where humans and other agents collaborate. Use the issue_* and project_* tools to keep that shared space in sync.',
@@ -19,6 +34,9 @@ const TEAMWORK_GUIDANCE = [
19
34
  export default function piTeamworkExtension(pi) {
20
35
  let provider;
21
36
  let readyPromise;
37
+ let sessionGeneration = 0;
38
+ const registeredToolNames = new Set();
39
+ const suspendedToolNames = new Set();
22
40
  async function ensureReady() {
23
41
  if (readyPromise)
24
42
  await readyPromise;
@@ -26,26 +44,80 @@ export default function piTeamworkExtension(pi) {
26
44
  return 'Teamwork provider is not initialized.';
27
45
  return undefined;
28
46
  }
47
+ function getProvider() {
48
+ if (!provider)
49
+ throw new Error('Teamwork provider is not initialized.');
50
+ return provider;
51
+ }
52
+ const teamworkTools = createTeamworkTools(ensureReady, getProvider);
53
+ function registerProviderTools(adapter) {
54
+ const supportedToolNames = supportedToolNamesForProvider(adapter);
55
+ for (const tool of teamworkTools) {
56
+ if (!supportedToolNames.has(tool.name) || registeredToolNames.has(tool.name)) {
57
+ continue;
58
+ }
59
+ pi.registerTool(tool);
60
+ registeredToolNames.add(tool.name);
61
+ }
62
+ alignActiveTeamworkTools(pi, supportedToolNames, suspendedToolNames);
63
+ }
64
+ function suspendTeamworkTools() {
65
+ alignActiveTeamworkTools(pi, new Set(), suspendedToolNames);
66
+ }
29
67
  pi.on('session_start', async (_event, ctx) => {
68
+ const generation = ++sessionGeneration;
30
69
  provider = undefined;
31
70
  readyPromise = undefined;
32
71
  const config = loadConfig(ctx.cwd);
33
72
  if (config.enabled === false) {
73
+ suspendTeamworkTools();
34
74
  ctx.ui.setStatus(STATUS_KEY, 'teamwork: disabled');
35
75
  return;
36
76
  }
77
+ const exec = async (command, args) => {
78
+ const result = await pi.exec(command, args, { cwd: ctx.cwd });
79
+ return { stdout: result.stdout, stderr: result.stderr, code: result.code };
80
+ };
37
81
  const providerName = config.provider ?? 'multica';
82
+ if (providerName === 'amaster') {
83
+ const amasterConfig = { ...(config.amaster ?? {}) };
84
+ const eventRecord = _event && typeof _event === 'object' && !Array.isArray(_event)
85
+ ? _event
86
+ : undefined;
87
+ const amasterEmployee = eventRecord?.amasterEmployee;
88
+ const sessionApiKey = amasterEmployee && typeof amasterEmployee === 'object' && !Array.isArray(amasterEmployee)
89
+ ? amasterEmployee.apiKey
90
+ : undefined;
91
+ const apiKey = typeof sessionApiKey === 'string' && sessionApiKey.trim()
92
+ ? sessionApiKey.trim()
93
+ : typeof amasterConfig.apiKey === 'string' && amasterConfig.apiKey.trim()
94
+ ? amasterConfig.apiKey.trim()
95
+ : undefined;
96
+ delete amasterConfig.apiKey;
97
+ const amasterExec = apiKey
98
+ ? (command, args) => command === 'amaster-employee'
99
+ ? execWithEnv(command, args, { AMASTER_BOARD_API_KEY: apiKey }, ctx.cwd)
100
+ : exec(command, args)
101
+ : exec;
102
+ const adapter = await initAmasterProvider(amasterConfig, amasterExec);
103
+ if (generation !== sessionGeneration)
104
+ return;
105
+ provider = adapter;
106
+ registerProviderTools(adapter);
107
+ ctx.ui.setStatus(STATUS_KEY, 'teamwork: amaster');
108
+ return;
109
+ }
38
110
  if (providerName !== 'multica') {
111
+ suspendTeamworkTools();
39
112
  ctx.ui.setStatus(STATUS_KEY, `teamwork: unknown provider "${providerName}"`);
40
113
  return;
41
114
  }
42
- const exec = async (command, args) => {
43
- const result = await pi.exec(command, args);
44
- return { stdout: result.stdout, stderr: result.stderr, code: result.code };
45
- };
46
115
  readyPromise = (async () => {
47
116
  const { adapter, installResult } = await initMulticaProvider(config.multica ?? {}, exec);
117
+ if (generation !== sessionGeneration)
118
+ return;
48
119
  provider = adapter;
120
+ registerProviderTools(adapter);
49
121
  if (!installResult.installed) {
50
122
  ctx.ui.notify(`multica CLI could not be installed: ${installResult.error ?? 'unknown error'}. Run "multica setup" manually.`, 'warning');
51
123
  ctx.ui.setStatus(STATUS_KEY, 'teamwork: multica (not installed)');
@@ -62,10 +134,13 @@ export default function piTeamworkExtension(pi) {
62
134
  }
63
135
  ctx.ui.setStatus(STATUS_KEY, 'teamwork: multica');
64
136
  })().catch((err) => {
137
+ if (generation !== sessionGeneration)
138
+ return;
65
139
  ctx.ui.setStatus(STATUS_KEY, `teamwork: multica (error: ${err instanceof Error ? err.message : String(err)})`);
66
140
  });
67
141
  });
68
142
  pi.on('session_shutdown', async () => {
143
+ sessionGeneration++;
69
144
  provider = undefined;
70
145
  readyPromise = undefined;
71
146
  });
@@ -98,202 +173,315 @@ export default function piTeamworkExtension(pi) {
98
173
  }
99
174
  },
100
175
  });
101
- // --- LLM-callable tools (registered once, use latest provider via closure) ---
102
- pi.registerTool({
103
- name: 'workspace_list',
104
- label: 'Teamwork',
105
- description: 'List all workspaces you belong to. Call this first to get a workspace ID before using other teamwork tools.',
106
- promptSnippet: 'List workspaces in the shared project tracker.',
107
- parameters: Type.Object({}),
108
- async execute() {
109
- const err = await ensureReady();
110
- if (err)
111
- return textResult(err);
112
- try {
113
- const workspaces = await provider.listWorkspaces();
114
- if (workspaces.length === 0)
115
- return textResult('No workspaces found.');
116
- return textResult(JSON.stringify(workspaces, null, 2));
117
- }
118
- catch (error) {
119
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
120
- }
121
- },
122
- });
123
- pi.registerTool({
124
- name: 'issue_list',
125
- label: 'Teamwork',
126
- description: 'List issues from a shared project tracker. Issues are work items that humans or other agents collaborate on. Supports filtering by status, assignee, and project.',
127
- promptSnippet: 'List issues from a shared project tracker where humans and agents collaborate.',
128
- parameters: Type.Object({
129
- workspaceId: Type.String({ description: 'Workspace ID (get from workspace_list).' }),
130
- status: Type.Optional(Type.String({ description: 'Filter by status (e.g. todo, in_progress, done, blocked).' })),
131
- assignee: Type.Optional(Type.String({ description: 'Filter by assignee name.' })),
132
- project: Type.Optional(Type.String({ description: 'Filter by project ID.' })),
133
- limit: Type.Optional(Type.Number({ description: 'Max number of results.' })),
176
+ }
177
+ function createTeamworkTools(ensureReady, getProvider) {
178
+ return [
179
+ defineTool({
180
+ name: 'workspace_list',
181
+ label: 'Teamwork',
182
+ description: 'List all workspaces you belong to. Use this to discover or switch workspaces. For the AMaster provider, workspaceId is the canonical AMaster companyId passed to the CLI as -C.',
183
+ promptSnippet: 'List workspaces in the shared project tracker.',
184
+ parameters: Type.Object({}),
185
+ async execute() {
186
+ const err = await ensureReady();
187
+ if (err)
188
+ return textResult(err);
189
+ try {
190
+ const workspaces = await getProvider().listWorkspaces();
191
+ if (workspaces.length === 0)
192
+ return textResult('No workspaces found.');
193
+ return textResult(JSON.stringify(workspaces, null, 2));
194
+ }
195
+ catch (error) {
196
+ return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
197
+ }
198
+ },
134
199
  }),
135
- async execute(_toolCallId, params) {
136
- const err = await ensureReady();
137
- if (err)
138
- return textResult(err);
139
- try {
140
- const issues = await provider.listIssues(params);
141
- if (issues.length === 0)
142
- return textResult('No issues found.');
143
- return textResult(JSON.stringify(issues, null, 2));
144
- }
145
- catch (error) {
146
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
147
- }
148
- },
149
- });
150
- pi.registerTool({
151
- name: 'issue_get',
152
- label: 'Teamwork',
153
- description: 'Get detailed information about a specific issue (a work item in the shared project tracker).',
154
- parameters: Type.Object({
155
- workspaceId: Type.String({ description: 'Workspace ID (get from workspace_list).' }),
156
- id: Type.String({ description: 'The issue ID.' }),
200
+ defineTool({
201
+ name: 'issue_list',
202
+ label: 'Teamwork',
203
+ description: 'List issues from a shared project tracker. Issues are work items that humans or other agents collaborate on. Supports filtering by status, assignee, and project.',
204
+ promptSnippet: 'List issues from a shared project tracker where humans and agents collaborate.',
205
+ parameters: Type.Object({
206
+ workspaceId: Type.Optional(Type.String({
207
+ description: 'Workspace ID. For AMaster provider this is the canonical companyId from workspace_list and is passed to the CLI as -C. Optional when the account has exactly one workspace.',
208
+ })),
209
+ status: Type.Optional(Type.String({ description: 'Filter by status (e.g. todo, in_progress, done, blocked).' })),
210
+ assignee: Type.Optional(Type.String({ description: 'Filter by assignee name.' })),
211
+ project: Type.Optional(Type.String({ description: 'Filter by project ID.' })),
212
+ limit: Type.Optional(Type.Number({ description: 'Max number of results.' })),
213
+ }),
214
+ async execute(_toolCallId, params) {
215
+ const err = await ensureReady();
216
+ if (err)
217
+ return textResult(err);
218
+ try {
219
+ const issues = await getProvider().listIssues(params);
220
+ if (issues.length === 0)
221
+ return textResult('No issues found.');
222
+ return textResult(JSON.stringify(issues, null, 2));
223
+ }
224
+ catch (error) {
225
+ return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
226
+ }
227
+ },
157
228
  }),
158
- async execute(_toolCallId, params) {
159
- const err = await ensureReady();
160
- if (err)
161
- return textResult(err);
162
- try {
163
- const issue = await provider.getIssue(params.id, params.workspaceId);
164
- if (!issue)
165
- return textResult(`Issue not found: ${params.id}`);
166
- return textResult(JSON.stringify(issue, null, 2));
167
- }
168
- catch (error) {
169
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
170
- }
171
- },
172
- });
173
- pi.registerTool({
174
- name: 'issue_create',
175
- label: 'Teamwork',
176
- description: 'Create a new issue (a work item / ticket) in the shared project tracker. Use this to file work for humans or other agents to pick up.',
177
- promptSnippet: 'Create issues / tickets in a shared project tracker for humans or agents to collaborate on.',
178
- parameters: Type.Object({
179
- workspaceId: Type.String({ description: 'Workspace ID (get from workspace_list).' }),
180
- title: Type.String({ description: 'Issue title.' }),
181
- description: Type.Optional(Type.String({ description: 'Issue description.' })),
182
- priority: Type.Optional(Type.String({ description: 'Priority (e.g. low, medium, high, urgent).' })),
183
- assignee: Type.Optional(Type.String({ description: 'Assignee name.' })),
184
- project: Type.Optional(Type.String({ description: 'Project ID to associate with.' })),
229
+ defineTool({
230
+ name: 'issue_get',
231
+ label: 'Teamwork',
232
+ description: 'Get detailed information about a specific issue (a work item in the shared project tracker).',
233
+ parameters: Type.Object({
234
+ workspaceId: Type.Optional(Type.String({
235
+ description: 'Workspace ID. For AMaster provider this is the canonical companyId from workspace_list and is passed to the CLI as -C. Optional when the account has exactly one workspace.',
236
+ })),
237
+ id: Type.String({ description: 'The issue ID.' }),
238
+ }),
239
+ async execute(_toolCallId, params) {
240
+ const err = await ensureReady();
241
+ if (err)
242
+ return textResult(err);
243
+ try {
244
+ const issue = await getProvider().getIssue(params.id, params.workspaceId);
245
+ if (!issue)
246
+ return textResult(`Issue not found: ${params.id}`);
247
+ return textResult(JSON.stringify(issue, null, 2));
248
+ }
249
+ catch (error) {
250
+ return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
251
+ }
252
+ },
185
253
  }),
186
- async execute(_toolCallId, params) {
187
- const err = await ensureReady();
188
- if (err)
189
- return textResult(err);
190
- try {
191
- const issue = await provider.createIssue(params);
192
- return textResult(JSON.stringify(issue, null, 2));
193
- }
194
- catch (error) {
195
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
196
- }
197
- },
198
- });
199
- pi.registerTool({
200
- name: 'issue_update',
201
- label: 'Teamwork',
202
- description: 'Update an existing issue in the shared project tracker. Can change title, description, status, priority, or assignee.',
203
- parameters: Type.Object({
204
- workspaceId: Type.String({ description: 'Workspace ID (get from workspace_list).' }),
205
- id: Type.String({ description: 'The issue ID to update.' }),
206
- title: Type.Optional(Type.String({ description: 'New title.' })),
207
- description: Type.Optional(Type.String({ description: 'New description.' })),
208
- status: Type.Optional(Type.String({
209
- description: 'New status (e.g. todo, in_progress, in_review, done, blocked, cancelled).',
210
- })),
211
- priority: Type.Optional(Type.String({ description: 'New priority.' })),
212
- assignee: Type.Optional(Type.String({ description: 'New assignee name.' })),
254
+ defineTool({
255
+ name: 'issue_create',
256
+ label: 'Teamwork',
257
+ description: 'Create a new issue (a work item / ticket) in the shared project tracker. Use this to file work for humans or other agents to pick up.',
258
+ promptSnippet: 'Create issues / tickets in a shared project tracker for humans or agents to collaborate on.',
259
+ parameters: Type.Object({
260
+ workspaceId: Type.Optional(Type.String({
261
+ description: 'Workspace ID. For AMaster provider this is the canonical companyId from workspace_list and is passed to the CLI as -C. Optional when the account has exactly one workspace.',
262
+ })),
263
+ title: Type.String({ description: 'Issue title.' }),
264
+ description: Type.Optional(Type.String({ description: 'Issue description.' })),
265
+ priority: Type.Optional(Type.String({ description: 'Priority (e.g. low, medium, high, urgent).' })),
266
+ assignee: Type.Optional(Type.String({ description: 'Assignee name.' })),
267
+ project: Type.Optional(Type.String({ description: 'Project ID to associate with.' })),
268
+ }),
269
+ async execute(_toolCallId, params) {
270
+ const err = await ensureReady();
271
+ if (err)
272
+ return textResult(err);
273
+ try {
274
+ const issue = await getProvider().createIssue(params);
275
+ return textResult(JSON.stringify(issue, null, 2));
276
+ }
277
+ catch (error) {
278
+ return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
279
+ }
280
+ },
213
281
  }),
214
- async execute(_toolCallId, params) {
215
- const err = await ensureReady();
216
- if (err)
217
- return textResult(err);
218
- const { id, workspaceId, ...input } = params;
219
- try {
220
- const issue = await provider.updateIssue(id, input, workspaceId);
221
- if (!issue)
222
- return textResult(`Issue not found: ${id}`);
223
- return textResult(JSON.stringify(issue, null, 2));
224
- }
225
- catch (error) {
226
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
227
- }
228
- },
229
- });
230
- pi.registerTool({
231
- name: 'issue_comment',
232
- label: 'Teamwork',
233
- description: 'Add a comment to an issue in the shared project tracker. Use for progress updates, questions, or blockers visible to other collaborators (humans or agents).',
234
- parameters: Type.Object({
235
- workspaceId: Type.String({ description: 'Workspace ID (get from workspace_list).' }),
236
- issueId: Type.String({ description: 'The issue ID to comment on.' }),
237
- content: Type.String({ description: 'Comment content.' }),
238
- parentId: Type.Optional(Type.String({ description: 'Parent comment ID for threaded replies.' })),
282
+ defineTool({
283
+ name: 'issue_update',
284
+ label: 'Teamwork',
285
+ description: 'Update an existing issue in the shared project tracker. Can change title, description, status, priority, or assignee.',
286
+ parameters: Type.Object({
287
+ workspaceId: Type.Optional(Type.String({
288
+ description: 'Workspace ID. For AMaster provider this is the canonical companyId from workspace_list and is passed to the CLI as -C. Optional when the account has exactly one workspace.',
289
+ })),
290
+ id: Type.String({ description: 'The issue ID to update.' }),
291
+ title: Type.Optional(Type.String({ description: 'New title.' })),
292
+ description: Type.Optional(Type.String({ description: 'New description.' })),
293
+ status: Type.Optional(Type.String({
294
+ description: 'New status (e.g. todo, in_progress, in_review, done, blocked, cancelled).',
295
+ })),
296
+ priority: Type.Optional(Type.String({ description: 'New priority.' })),
297
+ assignee: Type.Optional(Type.String({ description: 'New assignee name.' })),
298
+ }),
299
+ async execute(_toolCallId, params) {
300
+ const err = await ensureReady();
301
+ if (err)
302
+ return textResult(err);
303
+ const { id, workspaceId, ...input } = params;
304
+ try {
305
+ const issue = await getProvider().updateIssue(id, input, workspaceId);
306
+ if (!issue)
307
+ return textResult(`Issue not found: ${id}`);
308
+ return textResult(JSON.stringify(issue, null, 2));
309
+ }
310
+ catch (error) {
311
+ return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
312
+ }
313
+ },
239
314
  }),
240
- async execute(_toolCallId, params) {
241
- const err = await ensureReady();
242
- if (err)
243
- return textResult(err);
244
- try {
245
- const comment = await provider.addComment(params.issueId, params.content, params.parentId, params.workspaceId);
246
- return textResult(JSON.stringify(comment, null, 2));
247
- }
248
- catch (error) {
249
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
250
- }
251
- },
252
- });
253
- pi.registerTool({
254
- name: 'project_list',
255
- label: 'Teamwork',
256
- description: 'List all projects in a workspace.',
257
- parameters: Type.Object({
258
- workspaceId: Type.String({ description: 'Workspace ID (get from workspace_list).' }),
315
+ defineTool({
316
+ name: 'issue_comment',
317
+ label: 'Teamwork',
318
+ description: 'Add a comment to an issue in the shared project tracker. Use for progress updates, questions, or blockers visible to other collaborators (humans or agents).',
319
+ parameters: Type.Object({
320
+ workspaceId: Type.Optional(Type.String({
321
+ description: 'Workspace ID. For AMaster provider this is the canonical companyId from workspace_list and is passed to the CLI as -C. Optional when the account has exactly one workspace.',
322
+ })),
323
+ issueId: Type.String({ description: 'The issue ID to comment on.' }),
324
+ content: Type.String({ description: 'Comment content.' }),
325
+ parentId: Type.Optional(Type.String({
326
+ description: 'Optional provider-specific parent comment ID. The AMaster provider currently creates top-level comments.',
327
+ })),
328
+ }),
329
+ async execute(_toolCallId, params) {
330
+ const err = await ensureReady();
331
+ if (err)
332
+ return textResult(err);
333
+ try {
334
+ const comment = await getProvider().addComment(params.issueId, params.content, params.parentId, params.workspaceId);
335
+ return textResult(JSON.stringify(comment, null, 2));
336
+ }
337
+ catch (error) {
338
+ return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
339
+ }
340
+ },
259
341
  }),
260
- async execute(_toolCallId, params) {
261
- const err = await ensureReady();
262
- if (err)
263
- return textResult(err);
264
- try {
265
- const projects = await provider.listProjects(params.workspaceId);
266
- if (projects.length === 0)
267
- return textResult('No projects found.');
268
- return textResult(JSON.stringify(projects, null, 2));
269
- }
270
- catch (error) {
271
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
272
- }
273
- },
274
- });
275
- pi.registerTool({
276
- name: 'teamwork_status',
277
- label: 'Teamwork',
278
- description: 'Check the status of the teamwork collaboration provider (daemon status, connected agents, etc.).',
279
- parameters: Type.Object({}),
280
- async execute() {
281
- const err = await ensureReady();
282
- if (err)
283
- return textResult(err);
284
- try {
285
- const s = await provider.status();
286
- return textResult(JSON.stringify(s, null, 2));
342
+ defineTool({
343
+ name: 'project_list',
344
+ label: 'Teamwork',
345
+ description: 'List all projects in a workspace.',
346
+ parameters: Type.Object({
347
+ workspaceId: Type.Optional(Type.String({
348
+ description: 'Workspace ID. For AMaster provider this is the canonical companyId from workspace_list and is passed to the CLI as -C. Optional when the account has exactly one workspace.',
349
+ })),
350
+ }),
351
+ async execute(_toolCallId, params) {
352
+ const err = await ensureReady();
353
+ if (err)
354
+ return textResult(err);
355
+ try {
356
+ const projects = await getProvider().listProjects(params.workspaceId);
357
+ if (projects.length === 0)
358
+ return textResult('No projects found.');
359
+ return textResult(JSON.stringify(projects, null, 2));
360
+ }
361
+ catch (error) {
362
+ return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
363
+ }
364
+ },
365
+ }),
366
+ defineTool({
367
+ name: 'user_directory_list',
368
+ label: 'Teamwork',
369
+ description: 'List assignable AMaster users or members in the current teamwork workspace. This is read-only and helps choose assignees.',
370
+ parameters: Type.Object({
371
+ workspaceId: Type.Optional(Type.String({
372
+ description: 'Workspace ID. For AMaster provider this is the canonical companyId from workspace_list and is passed to the CLI as -C. Optional when the account has exactly one workspace.',
373
+ })),
374
+ q: Type.Optional(Type.String({ description: 'Optional search text.' })),
375
+ limit: Type.Optional(Type.Number({ description: 'Max number of results.' })),
376
+ }),
377
+ async execute(_toolCallId, params) {
378
+ const err = await ensureReady();
379
+ if (err)
380
+ return textResult(err);
381
+ const activeProvider = getProvider();
382
+ if (!activeProvider.listUserDirectory) {
383
+ return textResult('The current teamwork provider does not support user_directory_list.');
384
+ }
385
+ try {
386
+ const users = await activeProvider.listUserDirectory(params);
387
+ if (users.length === 0)
388
+ return textResult('No assignable users found.');
389
+ return textResult(JSON.stringify(users, null, 2));
390
+ }
391
+ catch (error) {
392
+ return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
393
+ }
394
+ },
395
+ }),
396
+ defineTool({
397
+ name: 'teamwork_status',
398
+ label: 'Teamwork',
399
+ description: 'Check the status of the teamwork collaboration provider (daemon status, connected agents, etc.).',
400
+ parameters: Type.Object({}),
401
+ async execute() {
402
+ const err = await ensureReady();
403
+ if (err)
404
+ return textResult(err);
405
+ try {
406
+ const s = await getProvider().status();
407
+ return textResult(JSON.stringify(s, null, 2));
408
+ }
409
+ catch (error) {
410
+ return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
411
+ }
412
+ },
413
+ }),
414
+ ];
415
+ }
416
+ function supportedToolNamesForProvider(provider) {
417
+ const supportedToolNames = new Set(TEAMWORK_TOOL_ORDER);
418
+ if (!provider.listUserDirectory)
419
+ supportedToolNames.delete('user_directory_list');
420
+ return supportedToolNames;
421
+ }
422
+ function alignActiveTeamworkTools(pi, supportedToolNames, suspendedToolNames) {
423
+ try {
424
+ const activeToolNames = pi.getActiveTools();
425
+ const nextActiveToolNames = [];
426
+ for (const toolName of activeToolNames) {
427
+ if (!TEAMWORK_TOOL_NAMES.has(toolName) || supportedToolNames.has(toolName)) {
428
+ nextActiveToolNames.push(toolName);
429
+ if (supportedToolNames.has(toolName))
430
+ suspendedToolNames.delete(toolName);
431
+ continue;
287
432
  }
288
- catch (error) {
289
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
433
+ suspendedToolNames.add(toolName);
434
+ }
435
+ for (const toolName of TEAMWORK_TOOL_ORDER) {
436
+ if (supportedToolNames.has(toolName) &&
437
+ suspendedToolNames.has(toolName) &&
438
+ !nextActiveToolNames.includes(toolName)) {
439
+ nextActiveToolNames.push(toolName);
440
+ suspendedToolNames.delete(toolName);
290
441
  }
291
- },
292
- });
442
+ }
443
+ if (nextActiveToolNames.join('\0') !== activeToolNames.join('\0')) {
444
+ pi.setActiveTools(nextActiveToolNames);
445
+ }
446
+ }
447
+ catch {
448
+ // Older test harnesses and pre-session runtimes may not expose active tool controls.
449
+ }
293
450
  }
294
451
  function textResult(text) {
295
452
  return { content: [{ type: 'text', text }], details: undefined };
296
453
  }
454
+ function execWithEnv(command, args, env, cwd) {
455
+ return new Promise((resolve) => {
456
+ const child = spawn(command, args, {
457
+ cwd,
458
+ shell: false,
459
+ stdio: ['ignore', 'pipe', 'pipe'],
460
+ env: { ...process.env, ...env },
461
+ });
462
+ let stdout = '';
463
+ let stderr = '';
464
+ let settled = false;
465
+ const finish = (result) => {
466
+ if (settled)
467
+ return;
468
+ settled = true;
469
+ resolve(result);
470
+ };
471
+ child.stdout?.on('data', (chunk) => {
472
+ stdout += String(chunk);
473
+ });
474
+ child.stderr?.on('data', (chunk) => {
475
+ stderr += String(chunk);
476
+ });
477
+ child.on('error', (error) => {
478
+ finish({ stdout, stderr: error.message, code: 1 });
479
+ });
480
+ child.on('close', (code) => {
481
+ finish({ stdout, stderr, code: code ?? 0 });
482
+ });
483
+ });
484
+ }
297
485
  function loadConfig(cwd) {
298
486
  try {
299
487
  const config = loadPiSettings(SETTINGS_KEY, {