@amaster.ai/pi-teamwork 0.1.2-beta.9 → 0.1.3

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,35 +1,165 @@
1
- import { loadPiSettings, resolveAgentDir } from '@amaster.ai/pi-shared/settings';
1
+ import { spawn } from 'node:child_process';
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);
21
+ const TEAMWORK_GUIDANCE = [
22
+ '<teamwork-guidance>',
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.',
24
+ '',
25
+ 'Before starting non-trivial work, check whether a relevant issue already exists (issue_list / issue_get) — to avoid duplicating work another collaborator has picked up.',
26
+ '',
27
+ 'When you reach meaningful progress, hit a blocker, or need input from a collaborator, leave a comment on the relevant issue (issue_comment). Comments are how humans and other agents observe what you are doing.',
28
+ '',
29
+ 'When you finish work that an issue describes, update its status (issue_update). An issue left in an outdated state misleads other collaborators.',
30
+ '',
31
+ 'The tracker is for cross-collaborator coordination, not for tracking your own session-local TODOs. Do not file an issue just to remind yourself of something within the current conversation.',
32
+ '</teamwork-guidance>',
33
+ ].join('\n');
6
34
  export default function piTeamworkExtension(pi) {
7
35
  let provider;
8
- const exec = async (command, args) => {
9
- const result = await pi.exec(command, args);
10
- return { stdout: result.stdout, stderr: result.stderr, code: result.code };
11
- };
36
+ let readyPromise;
37
+ let sessionGeneration = 0;
38
+ const registeredToolNames = new Set();
39
+ const suspendedToolNames = new Set();
40
+ async function ensureReady() {
41
+ if (readyPromise)
42
+ await readyPromise;
43
+ if (!provider)
44
+ return 'Teamwork provider is not initialized.';
45
+ return undefined;
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
+ }
12
67
  pi.on('session_start', async (_event, ctx) => {
68
+ const generation = ++sessionGeneration;
69
+ provider = undefined;
70
+ readyPromise = undefined;
13
71
  const config = loadConfig(ctx.cwd);
14
72
  if (config.enabled === false) {
73
+ suspendTeamworkTools();
15
74
  ctx.ui.setStatus(STATUS_KEY, 'teamwork: disabled');
16
75
  return;
17
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
+ };
18
81
  const providerName = config.provider ?? 'multica';
19
- if (providerName === 'multica') {
20
- provider = await initMulticaProvider(config.multica ?? {}, exec);
21
- ctx.ui.setStatus(STATUS_KEY, 'teamwork: 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;
22
109
  }
23
- else {
110
+ if (providerName !== 'multica') {
111
+ suspendTeamworkTools();
24
112
  ctx.ui.setStatus(STATUS_KEY, `teamwork: unknown provider "${providerName}"`);
113
+ return;
25
114
  }
115
+ readyPromise = (async () => {
116
+ const { adapter, installResult } = await initMulticaProvider(config.multica ?? {}, exec);
117
+ if (generation !== sessionGeneration)
118
+ return;
119
+ provider = adapter;
120
+ registerProviderTools(adapter);
121
+ if (!installResult.installed) {
122
+ ctx.ui.notify(`multica CLI could not be installed: ${installResult.error ?? 'unknown error'}. Run "multica setup" manually.`, 'warning');
123
+ ctx.ui.setStatus(STATUS_KEY, 'teamwork: multica (not installed)');
124
+ return;
125
+ }
126
+ if (!installResult.alreadyPresent) {
127
+ ctx.ui.notify('multica CLI was auto-installed successfully.', 'info');
128
+ }
129
+ if (!config.multica?.token && !config.multica?.serverUrl) {
130
+ ctx.ui.notify('No multica token or serverUrl configured. Run "multica setup" to authenticate.', 'warning');
131
+ }
132
+ if (config.multica?.serverUrl && !config.multica?.appUrl) {
133
+ ctx.ui.notify('multica serverUrl is set but appUrl is missing. Remote servers require both.', 'warning');
134
+ }
135
+ ctx.ui.setStatus(STATUS_KEY, 'teamwork: multica');
136
+ })().catch((err) => {
137
+ if (generation !== sessionGeneration)
138
+ return;
139
+ ctx.ui.setStatus(STATUS_KEY, `teamwork: multica (error: ${err instanceof Error ? err.message : String(err)})`);
140
+ });
26
141
  });
27
142
  pi.on('session_shutdown', async () => {
143
+ sessionGeneration++;
28
144
  provider = undefined;
145
+ readyPromise = undefined;
146
+ });
147
+ pi.on('before_agent_start', async (event) => {
148
+ if (readyPromise)
149
+ await readyPromise;
150
+ if (!provider)
151
+ return;
152
+ return {
153
+ systemPrompt: event.systemPrompt
154
+ ? `${event.systemPrompt}\n\n${TEAMWORK_GUIDANCE}`
155
+ : TEAMWORK_GUIDANCE,
156
+ };
29
157
  });
30
158
  pi.registerCommand('teamwork-status', {
31
159
  description: 'Show teamwork provider status.',
32
160
  handler: async (_args, ctx) => {
161
+ if (readyPromise)
162
+ await readyPromise;
33
163
  if (!provider) {
34
164
  ctx.ui.notify('Teamwork provider is not initialized.', 'warning');
35
165
  return;
@@ -43,172 +173,319 @@ export default function piTeamworkExtension(pi) {
43
173
  }
44
174
  },
45
175
  });
46
- // --- LLM-callable tools ---
47
- pi.registerTool({
48
- name: 'issue_list',
49
- label: 'Teamwork',
50
- description: 'List issues from the project management system. Supports filtering by status, assignee, and project.',
51
- promptSnippet: 'List issues from the team project tracker with optional filters.',
52
- parameters: Type.Object({
53
- status: Type.Optional(Type.String({ description: 'Filter by status (e.g. todo, in_progress, done, blocked).' })),
54
- assignee: Type.Optional(Type.String({ description: 'Filter by assignee name.' })),
55
- project: Type.Optional(Type.String({ description: 'Filter by project ID.' })),
56
- 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
+ },
57
199
  }),
58
- async execute(_toolCallId, params) {
59
- if (!provider)
60
- return textResult('Teamwork provider is not initialized.');
61
- try {
62
- const issues = await provider.listIssues(params);
63
- if (issues.length === 0)
64
- return textResult('No issues found.');
65
- return textResult(JSON.stringify(issues, null, 2));
66
- }
67
- catch (error) {
68
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
69
- }
70
- },
71
- });
72
- pi.registerTool({
73
- name: 'issue_get',
74
- label: 'Teamwork',
75
- description: 'Get detailed information about a specific issue.',
76
- parameters: Type.Object({
77
- 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
+ },
78
228
  }),
79
- async execute(_toolCallId, params) {
80
- if (!provider)
81
- return textResult('Teamwork provider is not initialized.');
82
- try {
83
- const issue = await provider.getIssue(params.id);
84
- if (!issue)
85
- return textResult(`Issue not found: ${params.id}`);
86
- return textResult(JSON.stringify(issue, null, 2));
87
- }
88
- catch (error) {
89
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
90
- }
91
- },
92
- });
93
- pi.registerTool({
94
- name: 'issue_create',
95
- label: 'Teamwork',
96
- description: 'Create a new issue in the project management system.',
97
- promptSnippet: 'Create issues in the team project tracker.',
98
- parameters: Type.Object({
99
- title: Type.String({ description: 'Issue title.' }),
100
- description: Type.Optional(Type.String({ description: 'Issue description.' })),
101
- priority: Type.Optional(Type.String({ description: 'Priority (e.g. low, medium, high, urgent).' })),
102
- assignee: Type.Optional(Type.String({ description: 'Assignee name.' })),
103
- 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
+ },
104
253
  }),
105
- async execute(_toolCallId, params) {
106
- if (!provider)
107
- return textResult('Teamwork provider is not initialized.');
108
- try {
109
- const issue = await provider.createIssue(params);
110
- return textResult(JSON.stringify(issue, null, 2));
111
- }
112
- catch (error) {
113
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
114
- }
115
- },
116
- });
117
- pi.registerTool({
118
- name: 'issue_update',
119
- label: 'Teamwork',
120
- description: 'Update an existing issue. Can change title, description, status, priority, or assignee.',
121
- parameters: Type.Object({
122
- id: Type.String({ description: 'The issue ID to update.' }),
123
- title: Type.Optional(Type.String({ description: 'New title.' })),
124
- description: Type.Optional(Type.String({ description: 'New description.' })),
125
- status: Type.Optional(Type.String({
126
- description: 'New status (e.g. todo, in_progress, in_review, done, blocked, cancelled).',
127
- })),
128
- priority: Type.Optional(Type.String({ description: 'New priority.' })),
129
- 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
+ },
130
281
  }),
131
- async execute(_toolCallId, params) {
132
- if (!provider)
133
- return textResult('Teamwork provider is not initialized.');
134
- const { id, ...input } = params;
135
- try {
136
- const issue = await provider.updateIssue(id, input);
137
- if (!issue)
138
- return textResult(`Issue not found: ${id}`);
139
- return textResult(JSON.stringify(issue, null, 2));
140
- }
141
- catch (error) {
142
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
143
- }
144
- },
145
- });
146
- pi.registerTool({
147
- name: 'issue_comment',
148
- label: 'Teamwork',
149
- description: 'Add a comment to an issue. Use for progress updates, questions, or blockers.',
150
- parameters: Type.Object({
151
- issueId: Type.String({ description: 'The issue ID to comment on.' }),
152
- content: Type.String({ description: 'Comment content.' }),
153
- 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
+ },
154
314
  }),
155
- async execute(_toolCallId, params) {
156
- if (!provider)
157
- return textResult('Teamwork provider is not initialized.');
158
- try {
159
- const comment = await provider.addComment(params.issueId, params.content, params.parentId);
160
- return textResult(JSON.stringify(comment, null, 2));
161
- }
162
- catch (error) {
163
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
164
- }
165
- },
166
- });
167
- pi.registerTool({
168
- name: 'project_list',
169
- label: 'Teamwork',
170
- description: 'List all projects in the workspace.',
171
- parameters: Type.Object({}),
172
- async execute() {
173
- if (!provider)
174
- return textResult('Teamwork provider is not initialized.');
175
- try {
176
- const projects = await provider.listProjects();
177
- if (projects.length === 0)
178
- return textResult('No projects found.');
179
- return textResult(JSON.stringify(projects, null, 2));
180
- }
181
- catch (error) {
182
- return textResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
183
- }
184
- },
185
- });
186
- pi.registerTool({
187
- name: 'teamwork_status',
188
- label: 'Teamwork',
189
- description: 'Check the status of the teamwork provider (daemon status, connected agents, etc.).',
190
- parameters: Type.Object({}),
191
- async execute() {
192
- if (!provider)
193
- return textResult('Teamwork provider is not initialized.');
194
- try {
195
- const s = await provider.status();
196
- return textResult(JSON.stringify(s, null, 2));
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
+ },
341
+ }),
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;
197
432
  }
198
- catch (error) {
199
- 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);
200
441
  }
201
- },
202
- });
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
+ }
203
450
  }
204
451
  function textResult(text) {
205
452
  return { content: [{ type: 'text', text }], details: undefined };
206
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
+ }
207
485
  function loadConfig(cwd) {
208
486
  try {
209
487
  const config = loadPiSettings(SETTINGS_KEY, {
210
488
  cwd,
211
- agentDir: resolveAgentDir(),
212
489
  });
213
490
  return Object.keys(config).length > 0 ? config : {};
214
491
  }