icopilot 2.2.0
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/CHANGELOG.md +250 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/icopilot.js +6 -0
- package/dist/acp/router.js +123 -0
- package/dist/acp/schema.js +53 -0
- package/dist/agents/aggregator.js +187 -0
- package/dist/agents/custom-agents.js +97 -0
- package/dist/agents/goal-driven.js +411 -0
- package/dist/agents/multi-repo.js +350 -0
- package/dist/agents/parallel-runner.js +181 -0
- package/dist/agents/router.js +144 -0
- package/dist/agents/self-heal.js +481 -0
- package/dist/agents/tdd-agent.js +278 -0
- package/dist/api/github-models.js +158 -0
- package/dist/bridge/ide-bridge.js +479 -0
- package/dist/cloud/routine-executor.js +34 -0
- package/dist/cloud/routine-scheduler.js +67 -0
- package/dist/cloud/routine-storage.js +297 -0
- package/dist/commands/acp-cmd.js +143 -0
- package/dist/commands/actions-cmd.js +624 -0
- package/dist/commands/agent-cmd.js +144 -0
- package/dist/commands/alias-cmd.js +132 -0
- package/dist/commands/bookmark-cmd.js +77 -0
- package/dist/commands/changelog-cmd.js +99 -0
- package/dist/commands/changes-cmd.js +120 -0
- package/dist/commands/clipboard-cmd.js +217 -0
- package/dist/commands/cloud-routine-cmd.js +265 -0
- package/dist/commands/codegen-cmd.js +544 -0
- package/dist/commands/compare-cmd.js +116 -0
- package/dist/commands/context-cmd.js +247 -0
- package/dist/commands/context-viz-cmd.js +43 -0
- package/dist/commands/conventions-cmd.js +116 -0
- package/dist/commands/cost-cmd.js +51 -0
- package/dist/commands/deps-cmd.js +294 -0
- package/dist/commands/diagram-cmd.js +658 -0
- package/dist/commands/diff-review-cmd.js +92 -0
- package/dist/commands/doc-cmd.js +412 -0
- package/dist/commands/doctor-cmd.js +152 -0
- package/dist/commands/editor-cmd.js +49 -0
- package/dist/commands/env-cmd.js +86 -0
- package/dist/commands/explain-cmd.js +78 -0
- package/dist/commands/explain-shell-cmd.js +22 -0
- package/dist/commands/explore-cmd.js +231 -0
- package/dist/commands/feedback-cmd.js +98 -0
- package/dist/commands/fix-cmd.js +17 -0
- package/dist/commands/generate-cmd.js +38 -0
- package/dist/commands/git-extra.js +197 -0
- package/dist/commands/git-log-cmd.js +98 -0
- package/dist/commands/git-undo-cmd.js +137 -0
- package/dist/commands/git.js +155 -0
- package/dist/commands/history-cmd.js +122 -0
- package/dist/commands/index-cmd.js +65 -0
- package/dist/commands/init-cmd.js +73 -0
- package/dist/commands/lint-cmd.js +133 -0
- package/dist/commands/memory-cmd.js +98 -0
- package/dist/commands/metrics-cmd.js +97 -0
- package/dist/commands/mode-prefix.js +30 -0
- package/dist/commands/multi-cmd.js +44 -0
- package/dist/commands/notify-cmd.js +204 -0
- package/dist/commands/profile-cmd.js +101 -0
- package/dist/commands/prompts.js +17 -0
- package/dist/commands/rag-cmd.js +60 -0
- package/dist/commands/readme-cmd.js +564 -0
- package/dist/commands/reasoning-cmd.js +34 -0
- package/dist/commands/refactor-cmd.js +96 -0
- package/dist/commands/release-cmd.js +450 -0
- package/dist/commands/repo-cmd.js +195 -0
- package/dist/commands/route-cmd.js +21 -0
- package/dist/commands/schedule-cmd.js +109 -0
- package/dist/commands/search-cmd.js +47 -0
- package/dist/commands/security-cmd.js +156 -0
- package/dist/commands/settings-cmd.js +238 -0
- package/dist/commands/skill-cmd.js +338 -0
- package/dist/commands/slash.js +2721 -0
- package/dist/commands/snippets-cmd.js +83 -0
- package/dist/commands/space-cmd.js +92 -0
- package/dist/commands/stash-cmd.js +156 -0
- package/dist/commands/stats-cmd.js +36 -0
- package/dist/commands/style-cmd.js +85 -0
- package/dist/commands/suggest-cmd.js +40 -0
- package/dist/commands/summary-cmd.js +138 -0
- package/dist/commands/task-cmd.js +58 -0
- package/dist/commands/team-memory-cmd.js +97 -0
- package/dist/commands/template-cmd.js +475 -0
- package/dist/commands/test-cmd.js +146 -0
- package/dist/commands/todo-cmd.js +172 -0
- package/dist/commands/tokens-cmd.js +277 -0
- package/dist/commands/trigger-cmd.js +147 -0
- package/dist/commands/undo-cmd.js +18 -0
- package/dist/commands/voice-cmd.js +89 -0
- package/dist/commands/watch-cmd.js +110 -0
- package/dist/commands/web-cmd.js +183 -0
- package/dist/commands/worktree-cmd.js +119 -0
- package/dist/config-profile.js +66 -0
- package/dist/config.js +288 -0
- package/dist/context/compactor.js +53 -0
- package/dist/context/dep-context.js +329 -0
- package/dist/context/file-refs.js +54 -0
- package/dist/context/git-context.js +229 -0
- package/dist/context/image-input.js +66 -0
- package/dist/context/memory.js +55 -0
- package/dist/context/persistent-memory.js +104 -0
- package/dist/context/pinned.js +96 -0
- package/dist/context/priority.js +150 -0
- package/dist/context/read-only.js +48 -0
- package/dist/context/smart-files.js +286 -0
- package/dist/context/team-memory.js +156 -0
- package/dist/extensions/loader.js +149 -0
- package/dist/extensions/marketplace.js +49 -0
- package/dist/extensions/slack-provider.js +181 -0
- package/dist/extensions/team.js +56 -0
- package/dist/extensions/teams-provider.js +222 -0
- package/dist/extensions/voice.js +18 -0
- package/dist/hooks/lifecycle.js +215 -0
- package/dist/hooks/precommit.js +463 -0
- package/dist/index/embeddings.js +23 -0
- package/dist/index/indexer.js +86 -0
- package/dist/index/retrieve.js +20 -0
- package/dist/index/store.js +95 -0
- package/dist/index.js +286 -0
- package/dist/intelligence/dead-code.js +457 -0
- package/dist/intelligence/error-watch.js +263 -0
- package/dist/intelligence/navigation.js +141 -0
- package/dist/intelligence/stack-trace.js +210 -0
- package/dist/intelligence/symbol-index.js +410 -0
- package/dist/knowledge/auto-memory.js +412 -0
- package/dist/knowledge/conventions.js +475 -0
- package/dist/knowledge/corrections.js +213 -0
- package/dist/knowledge/rag.js +450 -0
- package/dist/knowledge/style-learner.js +324 -0
- package/dist/logger.js +35 -0
- package/dist/mcp/client.js +144 -0
- package/dist/mcp/config.js +24 -0
- package/dist/mcp/index.js +89 -0
- package/dist/modes/auto-compact.js +20 -0
- package/dist/modes/autopilot.js +157 -0
- package/dist/modes/background.js +82 -0
- package/dist/modes/interactive.js +187 -0
- package/dist/modes/oneshot.js +36 -0
- package/dist/modes/tui.js +265 -0
- package/dist/modes/turn.js +342 -0
- package/dist/notifications/manager.js +107 -0
- package/dist/plugins/marketplace.js +244 -0
- package/dist/providers/custom-provider.js +298 -0
- package/dist/providers/local-model.js +121 -0
- package/dist/routing/profiles.js +44 -0
- package/dist/routing/router.js +18 -0
- package/dist/sandbox/container.js +151 -0
- package/dist/security/audit.js +237 -0
- package/dist/security/content-filter.js +449 -0
- package/dist/security/proxy.js +301 -0
- package/dist/security/retention.js +281 -0
- package/dist/security/roles.js +252 -0
- package/dist/server/api-server.js +679 -0
- package/dist/session/bookmarks.js +72 -0
- package/dist/session/cloud-session.js +291 -0
- package/dist/session/handoff.js +405 -0
- package/dist/session/manager.js +35 -0
- package/dist/session/session.js +296 -0
- package/dist/session/share.js +313 -0
- package/dist/session/undo-journal.js +91 -0
- package/dist/snippets/store.js +60 -0
- package/dist/spaces/space-config.js +156 -0
- package/dist/spaces/space.js +220 -0
- package/dist/stats/store.js +101 -0
- package/dist/tools/apply-patch.js +134 -0
- package/dist/tools/auto-check.js +218 -0
- package/dist/tools/diff-edit.js +150 -0
- package/dist/tools/diff-prompt.js +36 -0
- package/dist/tools/edit-file.js +66 -0
- package/dist/tools/file-ops.js +205 -0
- package/dist/tools/glob.js +17 -0
- package/dist/tools/grep.js +56 -0
- package/dist/tools/image.js +194 -0
- package/dist/tools/list-directory.js +228 -0
- package/dist/tools/memory.js +17 -0
- package/dist/tools/multi-edit.js +299 -0
- package/dist/tools/policy.js +95 -0
- package/dist/tools/registry.js +484 -0
- package/dist/tools/retry.js +74 -0
- package/dist/tools/run-in-terminal.js +162 -0
- package/dist/tools/safety.js +64 -0
- package/dist/tools/sandbox.js +15 -0
- package/dist/tools/search-symbols.js +212 -0
- package/dist/tools/shell.js +118 -0
- package/dist/tools/web.js +167 -0
- package/dist/ui/prompt.js +37 -0
- package/dist/ui/render.js +96 -0
- package/dist/ui/screen.js +13 -0
- package/dist/ui/theme.js +56 -0
- package/dist/util/browser.js +34 -0
- package/dist/util/completion.js +350 -0
- package/dist/util/cost.js +28 -0
- package/dist/util/keybindings.js +113 -0
- package/dist/util/lazy.js +26 -0
- package/dist/util/perf.js +25 -0
- package/dist/util/token-worker.js +11 -0
- package/dist/util/tokens.js +50 -0
- package/dist/workflows/builtins.js +128 -0
- package/dist/workflows/engine.js +496 -0
- package/dist/workflows/file-trigger.js +197 -0
- package/package.json +79 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export class SlackNotificationFormatter {
|
|
3
|
+
formatText(message) {
|
|
4
|
+
return {
|
|
5
|
+
type: 'section',
|
|
6
|
+
text: {
|
|
7
|
+
type: 'mrkdwn',
|
|
8
|
+
text: message,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
formatApprovalRequest(action, details) {
|
|
13
|
+
const blocks = [
|
|
14
|
+
{
|
|
15
|
+
type: 'section',
|
|
16
|
+
text: {
|
|
17
|
+
type: 'mrkdwn',
|
|
18
|
+
text: `🔐 *Approval Request* (ID: ${details.id || 'unknown'})`,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
type: 'section',
|
|
23
|
+
text: {
|
|
24
|
+
type: 'mrkdwn',
|
|
25
|
+
text: `*Action:* ${action}\n*Description:* ${details.description || 'N/A'}`,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: 'actions',
|
|
30
|
+
elements: [
|
|
31
|
+
{
|
|
32
|
+
type: 'button',
|
|
33
|
+
text: { type: 'plain_text', text: 'Approve' },
|
|
34
|
+
value: `approve-${details.id}`,
|
|
35
|
+
action_id: `approve-${details.id}`,
|
|
36
|
+
style: 'primary',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'button',
|
|
40
|
+
text: { type: 'plain_text', text: 'Deny' },
|
|
41
|
+
value: `deny-${details.id}`,
|
|
42
|
+
action_id: `deny-${details.id}`,
|
|
43
|
+
style: 'danger',
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
if (details.timeout && typeof details.timeout === 'number') {
|
|
49
|
+
const seconds = Math.ceil(details.timeout / 1000);
|
|
50
|
+
blocks.push({
|
|
51
|
+
type: 'context',
|
|
52
|
+
elements: [
|
|
53
|
+
{
|
|
54
|
+
type: 'mrkdwn',
|
|
55
|
+
text: `⏱️ Response required within ${seconds}s`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return { blocks };
|
|
61
|
+
}
|
|
62
|
+
formatError(error) {
|
|
63
|
+
return {
|
|
64
|
+
type: 'section',
|
|
65
|
+
text: {
|
|
66
|
+
type: 'mrkdwn',
|
|
67
|
+
text: `⚠️ *Error:* ${error}`,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export class SlackNotificationHandler {
|
|
73
|
+
token;
|
|
74
|
+
channel;
|
|
75
|
+
apiUrl = 'https://slack.com/api';
|
|
76
|
+
formatter = new SlackNotificationFormatter();
|
|
77
|
+
pendingApprovals = new Map();
|
|
78
|
+
webhookHandlers = new Map();
|
|
79
|
+
_connected = false;
|
|
80
|
+
constructor(token, channel) {
|
|
81
|
+
this.token = token;
|
|
82
|
+
this.channel = channel;
|
|
83
|
+
}
|
|
84
|
+
async notify(channel, message, metadata) {
|
|
85
|
+
const block = this.formatter.formatText(message);
|
|
86
|
+
const payload = {
|
|
87
|
+
channel,
|
|
88
|
+
blocks: [block],
|
|
89
|
+
};
|
|
90
|
+
if (metadata?.threadTs && typeof metadata.threadTs === 'string') {
|
|
91
|
+
payload.thread_ts = metadata.threadTs;
|
|
92
|
+
}
|
|
93
|
+
await this.callSlackApi('chat.postMessage', payload);
|
|
94
|
+
}
|
|
95
|
+
async requestApproval(channel, action, details) {
|
|
96
|
+
const id = randomUUID();
|
|
97
|
+
const timeout = details.timeout ?? 5 * 60 * 1000;
|
|
98
|
+
const formattedDetails = {
|
|
99
|
+
id,
|
|
100
|
+
description: details.description || 'Manual approval required',
|
|
101
|
+
};
|
|
102
|
+
if (timeout) {
|
|
103
|
+
formattedDetails.timeout = timeout;
|
|
104
|
+
}
|
|
105
|
+
const formatted = this.formatter.formatApprovalRequest(action, formattedDetails);
|
|
106
|
+
const payload = {
|
|
107
|
+
channel,
|
|
108
|
+
...formatted,
|
|
109
|
+
};
|
|
110
|
+
await this.callSlackApi('chat.postMessage', payload);
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
const timeoutHandle = setTimeout(() => {
|
|
113
|
+
this.pendingApprovals.delete(id);
|
|
114
|
+
resolve({ approved: false });
|
|
115
|
+
}, timeout);
|
|
116
|
+
const approval = {
|
|
117
|
+
id,
|
|
118
|
+
action,
|
|
119
|
+
timeout: timeoutHandle,
|
|
120
|
+
resolve,
|
|
121
|
+
};
|
|
122
|
+
this.pendingApprovals.set(id, approval);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
async getStatus() {
|
|
126
|
+
try {
|
|
127
|
+
const result = await this.callSlackApi('auth.test', {});
|
|
128
|
+
this._connected = result.ok === true;
|
|
129
|
+
return { connected: this._connected };
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
133
|
+
return { connected: false, error: msg };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
registerWebhook(url) {
|
|
137
|
+
process.env.ICOPILOT_SLACK_WEBHOOK_URL = url;
|
|
138
|
+
}
|
|
139
|
+
handleWebhookEvent(payload) {
|
|
140
|
+
const action = payload.actions;
|
|
141
|
+
if (!Array.isArray(action) || !action[0])
|
|
142
|
+
return;
|
|
143
|
+
const firstAction = action[0];
|
|
144
|
+
const actionId = firstAction.action_id;
|
|
145
|
+
if (!actionId || (!actionId.startsWith('approve-') && !actionId.startsWith('deny-')))
|
|
146
|
+
return;
|
|
147
|
+
const approved = actionId.startsWith('approve-');
|
|
148
|
+
const id = actionId.split('-')[1];
|
|
149
|
+
const pending = this.pendingApprovals.get(id);
|
|
150
|
+
if (!pending)
|
|
151
|
+
return;
|
|
152
|
+
clearTimeout(pending.timeout);
|
|
153
|
+
this.pendingApprovals.delete(id);
|
|
154
|
+
const user = payload.user?.id;
|
|
155
|
+
pending.resolve({
|
|
156
|
+
approved,
|
|
157
|
+
approver: user,
|
|
158
|
+
timestamp: new Date().toISOString(),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async callSlackApi(method, payload) {
|
|
162
|
+
const url = `${this.apiUrl}/${method}`;
|
|
163
|
+
const response = await fetch(url, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: {
|
|
166
|
+
'Content-Type': 'application/json',
|
|
167
|
+
Authorization: `Bearer ${this.token}`,
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify(payload),
|
|
170
|
+
});
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
throw new Error(`Slack API error: ${response.status} ${response.statusText}`);
|
|
173
|
+
}
|
|
174
|
+
const result = (await response.json());
|
|
175
|
+
if (result.ok !== true) {
|
|
176
|
+
const error = result.error;
|
|
177
|
+
throw new Error(`Slack API error: ${error || 'unknown'}`);
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const noopTeamTransport = {
|
|
2
|
+
async connect() {
|
|
3
|
+
throw new Error('Team mode transport not configured. See docs/future.md.');
|
|
4
|
+
},
|
|
5
|
+
async send() {
|
|
6
|
+
/* no-op */
|
|
7
|
+
},
|
|
8
|
+
on() {
|
|
9
|
+
/* no-op */
|
|
10
|
+
},
|
|
11
|
+
async disconnect() {
|
|
12
|
+
/* no-op */
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
const noopNotificationHandler = {
|
|
16
|
+
async notify() {
|
|
17
|
+
/* no-op */
|
|
18
|
+
},
|
|
19
|
+
async requestApproval() {
|
|
20
|
+
return { approved: false };
|
|
21
|
+
},
|
|
22
|
+
async getStatus() {
|
|
23
|
+
return { connected: false, error: 'Notifications not configured' };
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
let _transport = noopTeamTransport;
|
|
27
|
+
let _notificationHandler = noopNotificationHandler;
|
|
28
|
+
export function registerTeamTransport(t) {
|
|
29
|
+
_transport = t;
|
|
30
|
+
}
|
|
31
|
+
export function getTeamTransport() {
|
|
32
|
+
return _transport;
|
|
33
|
+
}
|
|
34
|
+
export function registerNotificationHandler(handler) {
|
|
35
|
+
_notificationHandler = handler;
|
|
36
|
+
}
|
|
37
|
+
export function getNotificationHandler() {
|
|
38
|
+
return _notificationHandler;
|
|
39
|
+
}
|
|
40
|
+
export function isTeamConfigured(config) {
|
|
41
|
+
return config != null && Boolean(config.token && config.channel);
|
|
42
|
+
}
|
|
43
|
+
export function getTeamConfig(env) {
|
|
44
|
+
const e = env || process.env;
|
|
45
|
+
const provider = e.ICOPILOT_NOTIFICATIONS_PROVIDER;
|
|
46
|
+
const token = e.ICOPILOT_NOTIFICATIONS_TOKEN;
|
|
47
|
+
const channel = e.ICOPILOT_NOTIFICATIONS_CHANNEL;
|
|
48
|
+
if (!provider || !token || !channel)
|
|
49
|
+
return null;
|
|
50
|
+
return {
|
|
51
|
+
provider,
|
|
52
|
+
token,
|
|
53
|
+
channel,
|
|
54
|
+
autoApprove: e.ICOPILOT_NOTIFICATIONS_AUTO_APPROVE?.split(',').filter(Boolean),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
export class TeamsNotificationFormatter {
|
|
3
|
+
formatText(message) {
|
|
4
|
+
return {
|
|
5
|
+
type: 'AdaptiveCard',
|
|
6
|
+
body: [
|
|
7
|
+
{
|
|
8
|
+
type: 'TextBlock',
|
|
9
|
+
text: message,
|
|
10
|
+
wrap: true,
|
|
11
|
+
spacing: 'Large',
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
formatApprovalRequest(action, details) {
|
|
17
|
+
const card = {
|
|
18
|
+
type: 'AdaptiveCard',
|
|
19
|
+
body: [
|
|
20
|
+
{
|
|
21
|
+
type: 'TextBlock',
|
|
22
|
+
text: '🔐 Approval Request',
|
|
23
|
+
weight: 'bolder',
|
|
24
|
+
size: 'large',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'FactSet',
|
|
28
|
+
facts: [
|
|
29
|
+
{
|
|
30
|
+
name: 'Action',
|
|
31
|
+
value: action,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'Description',
|
|
35
|
+
value: details.description || 'N/A',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'ID',
|
|
39
|
+
value: details.id || 'unknown',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
actions: [
|
|
45
|
+
{
|
|
46
|
+
type: 'Action.OpenUrl',
|
|
47
|
+
title: 'Approve',
|
|
48
|
+
url: `${details.approveUrl || ''}`,
|
|
49
|
+
style: 'positive',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: 'Action.OpenUrl',
|
|
53
|
+
title: 'Deny',
|
|
54
|
+
url: `${details.denyUrl || ''}`,
|
|
55
|
+
style: 'destructive',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
if (details.timeout && typeof details.timeout === 'number') {
|
|
60
|
+
const seconds = Math.ceil(details.timeout / 1000);
|
|
61
|
+
card.body.push({
|
|
62
|
+
type: 'TextBlock',
|
|
63
|
+
text: `⏱️ Response required within ${seconds}s`,
|
|
64
|
+
spacing: 'Large',
|
|
65
|
+
color: 'warning',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return card;
|
|
69
|
+
}
|
|
70
|
+
formatError(error) {
|
|
71
|
+
return {
|
|
72
|
+
type: 'AdaptiveCard',
|
|
73
|
+
body: [
|
|
74
|
+
{
|
|
75
|
+
type: 'TextBlock',
|
|
76
|
+
text: `⚠️ Error: ${error}`,
|
|
77
|
+
wrap: true,
|
|
78
|
+
color: 'attention',
|
|
79
|
+
weight: 'bolder',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export class TeamsNotificationHandler {
|
|
86
|
+
appId;
|
|
87
|
+
appPassword;
|
|
88
|
+
channelId;
|
|
89
|
+
formatter = new TeamsNotificationFormatter();
|
|
90
|
+
pendingApprovals = new Map();
|
|
91
|
+
_connected = false;
|
|
92
|
+
constructor(appId, appPassword, channelId) {
|
|
93
|
+
this.appId = appId;
|
|
94
|
+
this.appPassword = appPassword;
|
|
95
|
+
this.channelId = channelId;
|
|
96
|
+
}
|
|
97
|
+
async notify(channel, message, metadata) {
|
|
98
|
+
const card = this.formatter.formatText(message);
|
|
99
|
+
const payload = {
|
|
100
|
+
type: 'message',
|
|
101
|
+
from: { id: this.appId },
|
|
102
|
+
conversation: { id: channel },
|
|
103
|
+
attachments: [
|
|
104
|
+
{
|
|
105
|
+
contentType: 'application/vnd.microsoft.card.adaptive',
|
|
106
|
+
content: card,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
if (metadata?.replyToId && typeof metadata.replyToId === 'string') {
|
|
111
|
+
payload.replyToId = metadata.replyToId;
|
|
112
|
+
}
|
|
113
|
+
await this.callTeamsApi(channel, payload);
|
|
114
|
+
}
|
|
115
|
+
async requestApproval(channel, action, details) {
|
|
116
|
+
const id = randomUUID();
|
|
117
|
+
const timeout = details.timeout ?? 5 * 60 * 1000;
|
|
118
|
+
const webhookUrl = process.env.ICOPILOT_TEAMS_WEBHOOK_URL || '';
|
|
119
|
+
const baseUrl = webhookUrl.split('/connectors/')[0];
|
|
120
|
+
const formattedDetails = {
|
|
121
|
+
id,
|
|
122
|
+
description: details.description || 'Manual approval required',
|
|
123
|
+
approveUrl: `${baseUrl}/approve?id=${id}`,
|
|
124
|
+
denyUrl: `${baseUrl}/deny?id=${id}`,
|
|
125
|
+
};
|
|
126
|
+
if (timeout) {
|
|
127
|
+
formattedDetails.timeout = timeout;
|
|
128
|
+
}
|
|
129
|
+
const card = this.formatter.formatApprovalRequest(action, formattedDetails);
|
|
130
|
+
const payload = {
|
|
131
|
+
type: 'message',
|
|
132
|
+
from: { id: this.appId },
|
|
133
|
+
conversation: { id: channel },
|
|
134
|
+
attachments: [
|
|
135
|
+
{
|
|
136
|
+
contentType: 'application/vnd.microsoft.card.adaptive',
|
|
137
|
+
content: card,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
await this.callTeamsApi(channel, payload);
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
const timeoutHandle = setTimeout(() => {
|
|
144
|
+
this.pendingApprovals.delete(id);
|
|
145
|
+
resolve({ approved: false });
|
|
146
|
+
}, timeout);
|
|
147
|
+
const approval = {
|
|
148
|
+
id,
|
|
149
|
+
action,
|
|
150
|
+
timeout: timeoutHandle,
|
|
151
|
+
resolve,
|
|
152
|
+
};
|
|
153
|
+
this.pendingApprovals.set(id, approval);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async getStatus() {
|
|
157
|
+
try {
|
|
158
|
+
const token = await this.getAccessToken();
|
|
159
|
+
if (!token) {
|
|
160
|
+
return { connected: false, error: 'Failed to obtain access token' };
|
|
161
|
+
}
|
|
162
|
+
this._connected = true;
|
|
163
|
+
return { connected: true };
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
167
|
+
return { connected: false, error: msg };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
registerWebhook(url) {
|
|
171
|
+
process.env.ICOPILOT_TEAMS_WEBHOOK_URL = url;
|
|
172
|
+
}
|
|
173
|
+
handleApprovalResponse(id, approved, userId) {
|
|
174
|
+
const pending = this.pendingApprovals.get(id);
|
|
175
|
+
if (!pending)
|
|
176
|
+
return;
|
|
177
|
+
clearTimeout(pending.timeout);
|
|
178
|
+
this.pendingApprovals.delete(id);
|
|
179
|
+
pending.resolve({
|
|
180
|
+
approved,
|
|
181
|
+
approver: userId,
|
|
182
|
+
timestamp: new Date().toISOString(),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async getAccessToken() {
|
|
186
|
+
const url = 'https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token';
|
|
187
|
+
const params = new URLSearchParams({
|
|
188
|
+
client_id: this.appId,
|
|
189
|
+
client_secret: this.appPassword,
|
|
190
|
+
grant_type: 'client_credentials',
|
|
191
|
+
scope: 'https://api.botframework.com/.default',
|
|
192
|
+
});
|
|
193
|
+
const response = await fetch(url, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
196
|
+
body: params.toString(),
|
|
197
|
+
});
|
|
198
|
+
if (!response.ok) {
|
|
199
|
+
throw new Error(`Teams auth error: ${response.status}`);
|
|
200
|
+
}
|
|
201
|
+
const data = (await response.json());
|
|
202
|
+
const token = data.access_token;
|
|
203
|
+
if (!token)
|
|
204
|
+
throw new Error('No access token in response');
|
|
205
|
+
return token;
|
|
206
|
+
}
|
|
207
|
+
async callTeamsApi(channel, payload) {
|
|
208
|
+
const token = await this.getAccessToken();
|
|
209
|
+
const response = await fetch(`https://smba.trafficmanager.net/apis/v3/conversations/${channel}/activities`, {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: {
|
|
212
|
+
'Content-Type': 'application/json',
|
|
213
|
+
Authorization: `Bearer ${token}`,
|
|
214
|
+
},
|
|
215
|
+
body: JSON.stringify(payload),
|
|
216
|
+
});
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
throw new Error(`Teams API error: ${response.status} ${response.statusText}`);
|
|
219
|
+
}
|
|
220
|
+
return (await response.json());
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const noopSpeechProvider = {
|
|
2
|
+
async transcribe() {
|
|
3
|
+
throw new Error('Voice input not configured. Install a speech provider plugin (e.g., openai-whisper) with `/plugin install openai-whisper`.');
|
|
4
|
+
},
|
|
5
|
+
isConfigured() {
|
|
6
|
+
return false;
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
let _provider = noopSpeechProvider;
|
|
10
|
+
export function registerSpeechProvider(p) {
|
|
11
|
+
_provider = p;
|
|
12
|
+
}
|
|
13
|
+
export function getSpeechProvider() {
|
|
14
|
+
return _provider;
|
|
15
|
+
}
|
|
16
|
+
export function isVoiceInputConfigured() {
|
|
17
|
+
return _provider.isConfigured();
|
|
18
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
/**
|
|
6
|
+
* Loads lifecycle hook definitions and executes matching hooks for emitted events.
|
|
7
|
+
*/
|
|
8
|
+
export class HookManager {
|
|
9
|
+
hooks = [];
|
|
10
|
+
projectDir = process.cwd();
|
|
11
|
+
homeDir;
|
|
12
|
+
spawnFn;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
const defaultSpawn = (command, args, spawnOptions) => (spawnOptions
|
|
15
|
+
? spawn(command, args ?? [], spawnOptions)
|
|
16
|
+
: spawn(command, args ?? []));
|
|
17
|
+
this.homeDir = options.homeDir ?? os.homedir();
|
|
18
|
+
this.spawnFn = options.spawnFn ?? defaultSpawn;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Load hooks from the global ~/.icopilot/hooks.json file and the project-local
|
|
22
|
+
* .icopilot/hooks.json file. Project hooks are applied after global hooks.
|
|
23
|
+
*/
|
|
24
|
+
async loadHooks(projectDir) {
|
|
25
|
+
this.projectDir = projectDir;
|
|
26
|
+
const files = [
|
|
27
|
+
path.join(this.homeDir, '.icopilot', 'hooks.json'),
|
|
28
|
+
path.join(projectDir, '.icopilot', 'hooks.json'),
|
|
29
|
+
];
|
|
30
|
+
this.hooks = files.flatMap((filePath) => this.readHookFile(filePath));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Replace the active hook set. Intended for tests and in-memory scenarios.
|
|
34
|
+
*/
|
|
35
|
+
replaceHooks(hooks, projectDir = this.projectDir) {
|
|
36
|
+
this.projectDir = projectDir;
|
|
37
|
+
this.hooks = hooks.map((hook) => normalizeHookConfig(hook)).filter(isHookConfig);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Return the currently loaded hook definitions.
|
|
41
|
+
*/
|
|
42
|
+
getHooks() {
|
|
43
|
+
return this.hooks.map((hook) => ({ ...hook }));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Emit a lifecycle event. The payload is written to each hook's stdin as JSON.
|
|
47
|
+
* Hooks may deny the event, or modify the payload passed to later hooks.
|
|
48
|
+
*/
|
|
49
|
+
async emit(event, payload) {
|
|
50
|
+
const candidates = this.hooks.filter((hook) => hook.event === event);
|
|
51
|
+
if (!candidates.length)
|
|
52
|
+
return { action: 'continue' };
|
|
53
|
+
let activePayload = toRecord(payload);
|
|
54
|
+
let finalModifications;
|
|
55
|
+
let finalReason;
|
|
56
|
+
for (const hook of candidates) {
|
|
57
|
+
const result = await this.runHook(hook, activePayload);
|
|
58
|
+
if (result.reason)
|
|
59
|
+
finalReason = result.reason;
|
|
60
|
+
if (result.action === 'deny') {
|
|
61
|
+
return {
|
|
62
|
+
action: 'deny',
|
|
63
|
+
reason: result.reason ?? `hook denied ${event}`,
|
|
64
|
+
modifications: result.modifications,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (result.action === 'modify' && result.modifications) {
|
|
68
|
+
const normalized = toRecord(result.modifications);
|
|
69
|
+
activePayload = { ...activePayload, ...normalized };
|
|
70
|
+
finalModifications = { ...(finalModifications ?? {}), ...normalized };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (finalModifications && Object.keys(finalModifications).length > 0) {
|
|
74
|
+
return { action: 'modify', reason: finalReason, modifications: finalModifications };
|
|
75
|
+
}
|
|
76
|
+
return { action: 'continue', reason: finalReason };
|
|
77
|
+
}
|
|
78
|
+
readHookFile(filePath) {
|
|
79
|
+
try {
|
|
80
|
+
if (!fs.existsSync(filePath))
|
|
81
|
+
return [];
|
|
82
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
83
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.hooks))
|
|
84
|
+
return [];
|
|
85
|
+
return parsed.hooks.map((hook) => normalizeHookConfig(hook)).filter(isHookConfig);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
runHook(hook, payload) {
|
|
92
|
+
return new Promise((resolve) => {
|
|
93
|
+
const child = this.spawnFn(hook.command, [], {
|
|
94
|
+
cwd: this.projectDir,
|
|
95
|
+
shell: true,
|
|
96
|
+
windowsHide: true,
|
|
97
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
98
|
+
});
|
|
99
|
+
let stdout = '';
|
|
100
|
+
let stderr = '';
|
|
101
|
+
let settled = false;
|
|
102
|
+
const timeoutMs = hook.timeout ?? 5_000;
|
|
103
|
+
const timer = setTimeout(() => {
|
|
104
|
+
if (settled)
|
|
105
|
+
return;
|
|
106
|
+
settled = true;
|
|
107
|
+
try {
|
|
108
|
+
child.kill();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* ignore */
|
|
112
|
+
}
|
|
113
|
+
resolve({
|
|
114
|
+
action: 'continue',
|
|
115
|
+
reason: `hook timed out after ${timeoutMs}ms: ${hook.command}`,
|
|
116
|
+
});
|
|
117
|
+
}, timeoutMs);
|
|
118
|
+
child.stdout?.on('data', (chunk) => {
|
|
119
|
+
stdout += String(chunk);
|
|
120
|
+
});
|
|
121
|
+
child.stderr?.on('data', (chunk) => {
|
|
122
|
+
stderr += String(chunk);
|
|
123
|
+
});
|
|
124
|
+
child.on('error', (error) => {
|
|
125
|
+
if (settled)
|
|
126
|
+
return;
|
|
127
|
+
settled = true;
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
resolve({
|
|
130
|
+
action: 'continue',
|
|
131
|
+
reason: `hook failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
child.on('close', (code) => {
|
|
135
|
+
if (settled)
|
|
136
|
+
return;
|
|
137
|
+
settled = true;
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
resolve(parseHookOutput(stdout, stderr, code, hook.command));
|
|
140
|
+
});
|
|
141
|
+
child.stdin?.write(JSON.stringify(payload));
|
|
142
|
+
child.stdin?.end();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Shared lifecycle hook manager for the active CLI process.
|
|
148
|
+
*/
|
|
149
|
+
export const hookManager = new HookManager();
|
|
150
|
+
/**
|
|
151
|
+
* Load lifecycle hooks for the active project directory.
|
|
152
|
+
*/
|
|
153
|
+
export async function initializeLifecycleHooks(projectDir) {
|
|
154
|
+
await hookManager.loadHooks(projectDir);
|
|
155
|
+
}
|
|
156
|
+
function normalizeHookConfig(input) {
|
|
157
|
+
const candidate = input && typeof input === 'object' ? input : {};
|
|
158
|
+
return {
|
|
159
|
+
event: candidate.event,
|
|
160
|
+
command: typeof candidate.command === 'string' ? candidate.command : '',
|
|
161
|
+
timeout: typeof candidate.timeout === 'number' ? candidate.timeout : undefined,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function isHookConfig(hook) {
|
|
165
|
+
return (isHookEvent(hook.event) &&
|
|
166
|
+
typeof hook.command === 'string' &&
|
|
167
|
+
hook.command.trim().length > 0 &&
|
|
168
|
+
(hook.timeout === undefined || (Number.isFinite(hook.timeout) && hook.timeout > 0)));
|
|
169
|
+
}
|
|
170
|
+
function isHookEvent(value) {
|
|
171
|
+
return (value === 'sessionStart' ||
|
|
172
|
+
value === 'sessionEnd' ||
|
|
173
|
+
value === 'userPromptSubmit' ||
|
|
174
|
+
value === 'preToolUse' ||
|
|
175
|
+
value === 'postToolUse' ||
|
|
176
|
+
value === 'fileChanged' ||
|
|
177
|
+
value === 'cwdChanged' ||
|
|
178
|
+
value === 'preCompact' ||
|
|
179
|
+
value === 'postCompact' ||
|
|
180
|
+
value === 'errorOccurred');
|
|
181
|
+
}
|
|
182
|
+
function parseHookOutput(stdout, stderr, code, command) {
|
|
183
|
+
const trimmed = stdout.trim();
|
|
184
|
+
if (trimmed) {
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(trimmed);
|
|
187
|
+
if (parsed.action === 'continue' || parsed.action === 'deny' || parsed.action === 'modify') {
|
|
188
|
+
return {
|
|
189
|
+
action: parsed.action,
|
|
190
|
+
reason: typeof parsed.reason === 'string' ? parsed.reason : undefined,
|
|
191
|
+
modifications: parsed.modifications && typeof parsed.modifications === 'object'
|
|
192
|
+
? parsed.modifications
|
|
193
|
+
: undefined,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return { action: 'continue', reason: trimmed };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const stderrText = stderr.trim();
|
|
202
|
+
if (stderrText || code) {
|
|
203
|
+
return {
|
|
204
|
+
action: 'continue',
|
|
205
|
+
reason: stderrText || `hook exited with code ${String(code)}: ${command}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return { action: 'continue' };
|
|
209
|
+
}
|
|
210
|
+
function toRecord(value) {
|
|
211
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
212
|
+
return { ...value };
|
|
213
|
+
}
|
|
214
|
+
return {};
|
|
215
|
+
}
|