@gajae-code/coding-agent 0.12.6 → 0.12.8
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 +22 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-discovery-manager.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/resolve-config-value.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +17 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +8 -10
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
- package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +20 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
- package/dist/types/sdk/broker/broker.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -3
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
- package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
- package/dist/types/sdk/bus/notification-service.d.ts +31 -4
- package/dist/types/sdk/bus/session-control.d.ts +8 -3
- package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
- package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/host.d.ts +2 -23
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/providers.d.ts +22 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/session/artifacts.d.ts +5 -8
- package/dist/types/session/client-bridge.d.ts +0 -14
- package/dist/types/session/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -4
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/messages.d.ts +1 -4
- package/dist/types/session/session-manager.d.ts +7 -6
- package/dist/types/session/session-storage.d.ts +2 -0
- package/dist/types/session/streaming-output.d.ts +0 -32
- package/dist/types/task/receipt.d.ts +1 -2
- package/dist/types/task/types.d.ts +43 -178
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +0 -3
- package/dist/types/tools/output-meta.d.ts +5 -37
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/dist/types/tools/tool-result.d.ts +0 -1
- package/package.json +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/src/async/job-manager.ts +430 -46
- package/src/cli/notify-cli.ts +204 -94
- package/src/commands/notify.ts +19 -8
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-registry.ts +862 -75
- package/src/config/resolve-config-value.ts +10 -8
- package/src/config/settings-schema.ts +25 -15
- package/src/config/settings.ts +15 -15
- package/src/cursor.ts +13 -20
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +151 -425
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
- package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
- package/src/gjc-runtime/workflow-manifest.ts +0 -12
- package/src/internal-urls/agent-protocol.ts +38 -98
- package/src/internal-urls/artifact-protocol.ts +17 -28
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/internal-urls/registry-helpers.ts +0 -62
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/acp/acp-agent.ts +0 -24
- package/src/modes/acp/acp-client-bridge.ts +11 -154
- package/src/modes/components/hook-selector.ts +155 -44
- package/src/modes/components/model-selector.ts +242 -49
- package/src/modes/components/notifications-settings-editor.ts +631 -85
- package/src/modes/controllers/extension-ui-controller.ts +114 -15
- package/src/modes/controllers/selector-controller.ts +464 -50
- package/src/prompts/tools/bash.md +2 -2
- package/src/sdk/acp/adapter.ts +9 -200
- package/src/sdk/broker/broker.ts +54 -10
- package/src/sdk/broker/lifecycle-ledger.ts +34 -1
- package/src/sdk/broker/lifecycle.ts +47 -50
- package/src/sdk/bus/chat-daemon-cli.ts +12 -5
- package/src/sdk/bus/chat-daemon-control.ts +35 -8
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/discord-live-provider.ts +57 -2
- package/src/sdk/bus/discord-provider.ts +21 -0
- package/src/sdk/bus/index.ts +105 -359
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +431 -130
- package/src/sdk/bus/session-control.ts +119 -44
- package/src/sdk/bus/slack-live-provider.ts +78 -2
- package/src/sdk/bus/slack-provider.ts +24 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
- package/src/sdk/bus/telegram-daemon-control.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +23 -15
- package/src/sdk/client/discovery.ts +10 -4
- package/src/sdk/host/host.ts +9 -187
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/host/reverse-leases.ts +42 -249
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +5 -2
- package/src/sdk/providers.ts +51 -0
- package/src/sdk/session-directory.ts +1 -0
- package/src/sdk/session.ts +10 -7
- package/src/session/agent-session.ts +59 -202
- package/src/session/artifacts.ts +74 -211
- package/src/session/client-bridge.ts +0 -15
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +362 -124
- package/src/session/internal/native-publish-outcome.ts +3 -1
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/messages.ts +7 -32
- package/src/session/session-manager.ts +48 -15
- package/src/session/session-storage.ts +72 -5
- package/src/session/streaming-output.ts +40 -198
- package/src/slash-commands/builtin-registry.ts +82 -4
- package/src/task/index.ts +237 -65
- package/src/task/receipt.ts +2 -4
- package/src/task/render.ts +0 -30
- package/src/task/types.ts +10 -34
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +239 -818
- package/src/tools/eval.ts +4 -99
- package/src/tools/fetch.ts +15 -77
- package/src/tools/find.ts +3 -7
- package/src/tools/output-meta.ts +114 -696
- package/src/tools/search.ts +4 -9
- package/src/tools/subagent.ts +97 -75
- package/src/tools/telegram-send.ts +4 -2
- package/src/tools/todo-write.ts +47 -0
- package/src/tools/tool-result.ts +0 -1
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
- package/dist/types/utils/artifact-id.d.ts +0 -2
- package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
- package/src/utils/artifact-id.ts +0 -6
package/src/cli/notify-cli.ts
CHANGED
|
@@ -7,11 +7,18 @@ import { createInterface } from "node:readline/promises";
|
|
|
7
7
|
import { APP_NAME } from "@gajae-code/utils/dirs";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
import { Settings, type SettingsAtomicPatch } from "../config/settings";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ChatDaemonController,
|
|
12
|
+
type EnsureChatDaemonResult,
|
|
13
|
+
ensureDiscordDaemon,
|
|
14
|
+
ensureSlackDaemon,
|
|
15
|
+
} from "../sdk/bus/chat-daemon-control";
|
|
11
16
|
import { getNotificationConfig, maskToken, tokenFingerprint } from "../sdk/bus/config";
|
|
12
17
|
import {
|
|
13
18
|
clearTelegramActivationMarker,
|
|
14
19
|
createTelegramActivationMarker,
|
|
20
|
+
mutateNotificationProvider,
|
|
21
|
+
type NotificationProviderRuntimeAuthority,
|
|
15
22
|
observedTelegramActivationMarker,
|
|
16
23
|
type ProposedTelegramIdentity,
|
|
17
24
|
persistTelegramActivationMarker,
|
|
@@ -35,6 +42,7 @@ import {
|
|
|
35
42
|
resolveTelegramSetupPreflight,
|
|
36
43
|
} from "../sdk/bus/telegram-daemon";
|
|
37
44
|
import { runDaemonInternal } from "../sdk/bus/telegram-daemon-cli";
|
|
45
|
+
import { TelegramDaemonController } from "../sdk/bus/telegram-daemon-control";
|
|
38
46
|
import {
|
|
39
47
|
runTelegramSetup as runTelegramPairingSetup,
|
|
40
48
|
type TelegramSetupPreflight,
|
|
@@ -93,75 +101,99 @@ export interface NotifyCommandDeps {
|
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
|
|
96
|
-
if (args.length === 0 || args[0] !== "notify")
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
|
|
104
|
+
if (args.length === 0 || args[0] !== "notify") return undefined;
|
|
100
105
|
const action = args[1];
|
|
101
|
-
|
|
106
|
+
const providerValue = (value: string | undefined): NotifySetupProvider | undefined =>
|
|
107
|
+
value === "telegram" || value === "discord" || value === "slack" ? value : undefined;
|
|
108
|
+
const parseFlags = (
|
|
109
|
+
rest: string[],
|
|
110
|
+
valueFlags: ReadonlySet<string>,
|
|
111
|
+
booleanFlags: ReadonlySet<string>,
|
|
112
|
+
): Map<string, string | true> | undefined => {
|
|
113
|
+
const parsed = new Map<string, string | true>();
|
|
114
|
+
for (let index = 0; index < rest.length; index++) {
|
|
115
|
+
const flag = rest[index];
|
|
116
|
+
if (!flag?.startsWith("--") || parsed.has(flag)) return undefined;
|
|
117
|
+
if (booleanFlags.has(flag)) {
|
|
118
|
+
parsed.set(flag, true);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (!valueFlags.has(flag)) return undefined;
|
|
122
|
+
const value = rest[++index];
|
|
123
|
+
if (!value || value.startsWith("--")) return undefined;
|
|
124
|
+
parsed.set(flag, value);
|
|
125
|
+
}
|
|
126
|
+
return parsed;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (action === "setup") {
|
|
102
130
|
const rest = args.slice(2);
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
131
|
+
const positional = rest[0]?.startsWith("--") ? undefined : rest.shift();
|
|
132
|
+
const provider = positional === undefined ? undefined : providerValue(positional);
|
|
133
|
+
if (positional !== undefined && !provider) return undefined;
|
|
134
|
+
const flags = parseFlags(
|
|
135
|
+
rest,
|
|
136
|
+
new Set([
|
|
137
|
+
"--token",
|
|
138
|
+
"--chat-id",
|
|
139
|
+
"--discord-bot-token",
|
|
140
|
+
"--discord-application-id",
|
|
141
|
+
"--discord-guild-id",
|
|
142
|
+
"--discord-parent-channel-id",
|
|
143
|
+
"--slack-bot-token",
|
|
144
|
+
"--slack-app-token",
|
|
145
|
+
"--slack-workspace-id",
|
|
146
|
+
"--slack-channel-id",
|
|
147
|
+
"--slack-authorized-user-id",
|
|
148
|
+
]),
|
|
149
|
+
new Set(["--redact"]),
|
|
150
|
+
);
|
|
151
|
+
if (!flags) return undefined;
|
|
152
|
+
const value = (name: string): string | undefined => {
|
|
153
|
+
const found = flags.get(name);
|
|
154
|
+
return typeof found === "string" ? found : undefined;
|
|
106
155
|
};
|
|
107
|
-
const valueFlags = [
|
|
108
|
-
"--token",
|
|
109
|
-
"--chat-id",
|
|
110
|
-
"--discord-bot-token",
|
|
111
|
-
"--discord-application-id",
|
|
112
|
-
"--discord-guild-id",
|
|
113
|
-
"--discord-parent-channel-id",
|
|
114
|
-
"--slack-bot-token",
|
|
115
|
-
"--slack-app-token",
|
|
116
|
-
"--slack-workspace-id",
|
|
117
|
-
"--slack-channel-id",
|
|
118
|
-
"--slack-authorized-user-id",
|
|
119
|
-
];
|
|
120
|
-
if (
|
|
121
|
-
valueFlags.some(name => {
|
|
122
|
-
const index = rest.indexOf(name);
|
|
123
|
-
const value = index >= 0 ? rest[index + 1] : undefined;
|
|
124
|
-
return index >= 0 && (!value || value.startsWith("--"));
|
|
125
|
-
})
|
|
126
|
-
)
|
|
127
|
-
return undefined;
|
|
128
|
-
const provider = rest[0]?.startsWith("--") ? undefined : rest[0];
|
|
129
|
-
if (provider !== undefined && provider !== "telegram" && provider !== "discord" && provider !== "slack") {
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
156
|
return {
|
|
133
157
|
action,
|
|
134
|
-
rawArgs:
|
|
158
|
+
rawArgs: args.slice(2),
|
|
135
159
|
...(provider ? { provider } : {}),
|
|
136
|
-
token:
|
|
137
|
-
chatId:
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
...(flag("--slack-authorized-user-id") ? { slackAuthorizedUserId: flag("--slack-authorized-user-id") } : {}),
|
|
149
|
-
redact: rest.includes("--redact"),
|
|
160
|
+
token: value("--token"),
|
|
161
|
+
chatId: value("--chat-id"),
|
|
162
|
+
discordBotToken: value("--discord-bot-token"),
|
|
163
|
+
discordApplicationId: value("--discord-application-id"),
|
|
164
|
+
discordGuildId: value("--discord-guild-id"),
|
|
165
|
+
discordParentChannelId: value("--discord-parent-channel-id"),
|
|
166
|
+
slackBotToken: value("--slack-bot-token"),
|
|
167
|
+
slackAppToken: value("--slack-app-token"),
|
|
168
|
+
slackWorkspaceId: value("--slack-workspace-id"),
|
|
169
|
+
slackChannelId: value("--slack-channel-id"),
|
|
170
|
+
slackAuthorizedUserId: value("--slack-authorized-user-id"),
|
|
171
|
+
redact: flags.get("--redact") === true,
|
|
150
172
|
};
|
|
151
173
|
}
|
|
152
|
-
if (action === "
|
|
174
|
+
if (action === "status") {
|
|
175
|
+
return args.length === 2 ? { action, rawArgs: [] } : undefined;
|
|
176
|
+
}
|
|
177
|
+
if (action === "health" || action === "test") {
|
|
153
178
|
const rest = args.slice(2);
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
179
|
+
const flags = parseFlags(
|
|
180
|
+
rest,
|
|
181
|
+
new Set(action === "health" ? ["--provider"] : ["--provider", "--message"]),
|
|
182
|
+
new Set(action === "health" ? ["--probe"] : []),
|
|
183
|
+
);
|
|
184
|
+
if (!flags) return undefined;
|
|
185
|
+
const rawProvider = flags.get("--provider");
|
|
186
|
+
const provider = typeof rawProvider === "string" ? providerValue(rawProvider) : undefined;
|
|
187
|
+
if (rawProvider !== undefined && !provider) return undefined;
|
|
158
188
|
return {
|
|
159
189
|
action,
|
|
160
190
|
rawArgs: rest,
|
|
161
|
-
|
|
162
|
-
|
|
191
|
+
...(provider ? { provider } : {}),
|
|
192
|
+
probe: flags.get("--probe") === true,
|
|
193
|
+
message: typeof flags.get("--message") === "string" ? (flags.get("--message") as string) : undefined,
|
|
163
194
|
};
|
|
164
195
|
}
|
|
196
|
+
if (action === "recovery") return args.length === 2 ? { action, rawArgs: [] } : undefined;
|
|
165
197
|
if (action === "daemon-internal") {
|
|
166
198
|
return {
|
|
167
199
|
action,
|
|
@@ -169,7 +201,6 @@ export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
|
|
|
169
201
|
rawArgs: args.slice(2),
|
|
170
202
|
};
|
|
171
203
|
}
|
|
172
|
-
|
|
173
204
|
return undefined;
|
|
174
205
|
}
|
|
175
206
|
|
|
@@ -273,18 +304,46 @@ async function runDiscordSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps):
|
|
|
273
304
|
deps,
|
|
274
305
|
);
|
|
275
306
|
const settings = await getSettings(deps);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
307
|
+
let activationFailure: string | undefined;
|
|
308
|
+
let activationOutcome: EnsureChatDaemonResult | undefined;
|
|
309
|
+
const runtime: NotificationProviderRuntimeAuthority = {
|
|
310
|
+
activate: async provider => {
|
|
311
|
+
if (provider !== "discord") throw new Error("Unexpected provider activation request.");
|
|
312
|
+
try {
|
|
313
|
+
const result = await ensureConfiguredProviderDaemon("discord", settings, deps);
|
|
314
|
+
if (result === "disabled") throw new Error("Discord runtime did not activate.");
|
|
315
|
+
activationOutcome = result;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
activationFailure = error instanceof Error ? error.message : "Discord runtime activation failed.";
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
deactivate: async () => undefined,
|
|
322
|
+
};
|
|
323
|
+
const result = await mutateNotificationProvider({
|
|
324
|
+
settings,
|
|
325
|
+
mutation: {
|
|
326
|
+
provider: "discord",
|
|
327
|
+
botToken: { action: "replace", value: botToken },
|
|
328
|
+
applicationId,
|
|
329
|
+
guildId,
|
|
330
|
+
parentChannelId,
|
|
331
|
+
},
|
|
332
|
+
configureAndActivate: true,
|
|
333
|
+
...(cmd.redact ? { redact: true } : {}),
|
|
334
|
+
runtime,
|
|
335
|
+
});
|
|
336
|
+
if (result.status === "commit_failed")
|
|
337
|
+
throw new Error("Discord configuration was not saved because the CAS commit failed.");
|
|
338
|
+
if (result.status !== "activated") {
|
|
339
|
+
const detail = `runtime activation failed: ${activationFailure ?? result.status}`;
|
|
340
|
+
process.stderr.write(`Discord configuration saved, but ${detail}.\n`);
|
|
341
|
+
if (deps.setExitCode) deps.setExitCode(1);
|
|
342
|
+
else process.exitCode = 1;
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
286
345
|
process.stdout.write(
|
|
287
|
-
`Discord
|
|
346
|
+
`Discord configuration saved and activated. botToken=${maskToken(botToken)} applicationId=${applicationId} guildId=${guildId} parentChannelId=${parentChannelId} daemon=${activationOutcome ?? "attached"}\n`,
|
|
288
347
|
);
|
|
289
348
|
}
|
|
290
349
|
|
|
@@ -295,21 +354,47 @@ async function runSlackSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps): P
|
|
|
295
354
|
const channelId = await promptSetupValue(cmd.slackChannelId, "--slack-channel-id", false, deps);
|
|
296
355
|
const authorizedUserId = cmd.slackAuthorizedUserId?.trim() || undefined;
|
|
297
356
|
const settings = await getSettings(deps);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
357
|
+
let activationFailure: string | undefined;
|
|
358
|
+
let activationOutcome: EnsureChatDaemonResult | undefined;
|
|
359
|
+
const runtime: NotificationProviderRuntimeAuthority = {
|
|
360
|
+
activate: async provider => {
|
|
361
|
+
if (provider !== "slack") throw new Error("Unexpected provider activation request.");
|
|
362
|
+
try {
|
|
363
|
+
const result = await ensureConfiguredProviderDaemon("slack", settings, deps);
|
|
364
|
+
if (result === "disabled") throw new Error("Slack runtime did not activate.");
|
|
365
|
+
activationOutcome = result;
|
|
366
|
+
} catch (error) {
|
|
367
|
+
activationFailure = error instanceof Error ? error.message : "Slack runtime activation failed.";
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
deactivate: async () => undefined,
|
|
372
|
+
};
|
|
373
|
+
const result = await mutateNotificationProvider({
|
|
374
|
+
settings,
|
|
375
|
+
mutation: {
|
|
376
|
+
provider: "slack",
|
|
377
|
+
botToken: { action: "replace", value: botToken },
|
|
378
|
+
appToken: { action: "replace", value: appToken },
|
|
379
|
+
workspaceId,
|
|
380
|
+
channelId,
|
|
381
|
+
authorizedUserId,
|
|
382
|
+
},
|
|
383
|
+
configureAndActivate: true,
|
|
384
|
+
...(cmd.redact ? { redact: true } : {}),
|
|
385
|
+
runtime,
|
|
386
|
+
});
|
|
387
|
+
if (result.status === "commit_failed")
|
|
388
|
+
throw new Error("Slack configuration was not saved because the CAS commit failed.");
|
|
389
|
+
if (result.status !== "activated") {
|
|
390
|
+
const detail = `runtime activation failed: ${activationFailure ?? result.status}`;
|
|
391
|
+
process.stderr.write(`Slack configuration saved, but ${detail}.\n`);
|
|
392
|
+
if (deps.setExitCode) deps.setExitCode(1);
|
|
393
|
+
else process.exitCode = 1;
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
311
396
|
process.stdout.write(
|
|
312
|
-
`Slack
|
|
397
|
+
`Slack configuration saved and activated. botToken=${maskToken(botToken)} appToken=${maskToken(appToken)} workspaceId=${workspaceId} channelId=${channelId} authorizedUserId=${authorizedUserId ?? "(unset; inbound denied)"} daemon=${activationOutcome ?? "attached"}\n`,
|
|
313
398
|
);
|
|
314
399
|
}
|
|
315
400
|
|
|
@@ -365,6 +450,7 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
365
450
|
if (result.pairingSource === "provided") {
|
|
366
451
|
process.stdout.write(`Using provided chat id ${result.chatId} (non-interactive).\n`);
|
|
367
452
|
}
|
|
453
|
+
let settingsCommitted = false;
|
|
368
454
|
try {
|
|
369
455
|
const proposedIdentity = deps.setupPreflight
|
|
370
456
|
? proposedIdentityFromSetupPreflight(deps.setupPreflight, token.trim(), result.chatId)
|
|
@@ -385,9 +471,11 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
385
471
|
{ path: "notifications.telegram.botToken", op: "set", value: token.trim() },
|
|
386
472
|
{ path: "notifications.telegram.chatId", op: "set", value: result.chatId },
|
|
387
473
|
{ path: "notifications.enabled", op: "set", value: true },
|
|
474
|
+
{ path: "notifications.telegram.enabled", op: "set", value: true },
|
|
388
475
|
];
|
|
389
476
|
if (deps.setupRedact ?? cmd.redact) patches.push({ path: "notifications.redact", op: "set", value: true });
|
|
390
477
|
const receipt = await settings.commitAtomicBatch(patches);
|
|
478
|
+
settingsCommitted = true;
|
|
391
479
|
const activationMarker = createTelegramActivationMarker({
|
|
392
480
|
botToken: token.trim(),
|
|
393
481
|
chatId: result.chatId,
|
|
@@ -422,6 +510,7 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
422
510
|
});
|
|
423
511
|
if (activation.status === "blocked_identity") {
|
|
424
512
|
const restored = await activation.restore();
|
|
513
|
+
if (restored.status === "restored" || restored.status === "still_blocked") settingsCommitted = false;
|
|
425
514
|
const detail =
|
|
426
515
|
restored.status === "restored"
|
|
427
516
|
? "Telegram activation was blocked by a foreign daemon; previous settings were restored."
|
|
@@ -432,9 +521,18 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
432
521
|
: "Telegram activation was blocked; refusing to report setup success.";
|
|
433
522
|
throw new Error(detail);
|
|
434
523
|
}
|
|
524
|
+
if (activation.status === "activation_failed") {
|
|
525
|
+
receipt.discard();
|
|
526
|
+
throw new Error(activation.message);
|
|
527
|
+
}
|
|
528
|
+
receipt.discard();
|
|
435
529
|
} catch (error) {
|
|
436
530
|
const detail = sanitizeDiagnostic(error instanceof Error ? error.message : "unknown persistence failure", token);
|
|
437
|
-
throw new Error(
|
|
531
|
+
throw new Error(
|
|
532
|
+
settingsCommitted
|
|
533
|
+
? `Telegram notification settings were saved, but activation or recovery failed: ${detail}`
|
|
534
|
+
: `Unable to persist and activate Telegram notification settings: ${detail}`,
|
|
535
|
+
);
|
|
438
536
|
}
|
|
439
537
|
process.stdout.write(
|
|
440
538
|
`Notifications enabled. botToken=${maskToken(token)} chatId=${result.chatId} threaded=${result.threadedLabel}\n`,
|
|
@@ -586,6 +684,7 @@ async function runHealth(deps: NotifyCommandDeps, cmd: NotifyCommandArgs): Promi
|
|
|
586
684
|
const settings = await getSettings(deps);
|
|
587
685
|
const report = await checkNotificationHealth({
|
|
588
686
|
settings,
|
|
687
|
+
provider: cmd.provider,
|
|
589
688
|
probe: cmd.probe,
|
|
590
689
|
deps: { fetchImpl: deps.fetchImpl, apiBase: deps.apiBase },
|
|
591
690
|
});
|
|
@@ -598,8 +697,19 @@ async function runTest(deps: NotifyCommandDeps, cmd: NotifyCommandArgs): Promise
|
|
|
598
697
|
const settings = await getSettings(deps);
|
|
599
698
|
const result = await sendNotificationTest({
|
|
600
699
|
settings,
|
|
700
|
+
provider: cmd.provider,
|
|
601
701
|
text: cmd.message,
|
|
602
|
-
deps: {
|
|
702
|
+
deps: {
|
|
703
|
+
fetchImpl: deps.fetchImpl,
|
|
704
|
+
apiBase: deps.apiBase,
|
|
705
|
+
providerRuntimeStatus: async provider => {
|
|
706
|
+
const status =
|
|
707
|
+
provider === "telegram"
|
|
708
|
+
? await new TelegramDaemonController(settings).status()
|
|
709
|
+
: await new ChatDaemonController(settings, provider).status();
|
|
710
|
+
return status.health === "running" ? "ready" : "inactive";
|
|
711
|
+
},
|
|
712
|
+
},
|
|
603
713
|
});
|
|
604
714
|
process.stdout.write(`${formatNotificationTestResult(result)}\n`);
|
|
605
715
|
if (!result.ok && deps.setExitCode) deps.setExitCode(1);
|
|
@@ -616,8 +726,8 @@ export function printNotifyHelp(): void {
|
|
|
616
726
|
process.stdout.write(`${chalk.bold(`${APP_NAME} notify`)} - Configure Telegram, Discord, or Slack notifications
|
|
617
727
|
|
|
618
728
|
${chalk.bold("Interactive path:")}
|
|
619
|
-
In a running GJC session, use /settings → Notifications for
|
|
620
|
-
|
|
729
|
+
In a running GJC session, use /settings → Notifications for first-class Telegram, Discord,
|
|
730
|
+
and Slack configure/edit/repair, desired intent, health, test, removal, global master, and session controls.
|
|
621
731
|
The CLI subcommands below remain the authoritative headless and automation fallback.
|
|
622
732
|
|
|
623
733
|
${chalk.bold("Usage:")}
|
|
@@ -625,15 +735,15 @@ ${chalk.bold("Usage:")}
|
|
|
625
735
|
${APP_NAME} notify setup discord --discord-bot-token <token> --discord-application-id <id> --discord-guild-id <id> --discord-parent-channel-id <id>
|
|
626
736
|
${APP_NAME} notify setup slack --slack-bot-token <token> --slack-app-token <token> --slack-workspace-id <id> --slack-channel-id <id> [--slack-authorized-user-id <id>]
|
|
627
737
|
${APP_NAME} notify status
|
|
628
|
-
${APP_NAME} notify health [--probe]
|
|
629
|
-
${APP_NAME} notify test [--message <text>]
|
|
738
|
+
${APP_NAME} notify health [--provider telegram|discord|slack] [--probe]
|
|
739
|
+
${APP_NAME} notify test [--provider telegram|discord|slack] [--message <text>]
|
|
630
740
|
${APP_NAME} notify recovery
|
|
631
741
|
|
|
632
742
|
${chalk.bold("Subcommands:")}
|
|
633
|
-
setup Pair Telegram or save complete
|
|
634
|
-
status Show
|
|
635
|
-
health Report
|
|
636
|
-
test Send a one-off test
|
|
743
|
+
setup Pair Telegram or atomically save and activate complete Discord/Slack settings
|
|
744
|
+
status Show global master and provider configured/repair/desired/effective state without secrets
|
|
745
|
+
health Report selected provider state; --probe uses REST only and never opens Gateway/Socket Mode
|
|
746
|
+
test Send a one-off test through one selected or uniquely effective provider
|
|
637
747
|
recovery Clear dead-owner daemon locks and stale per-session endpoint files (never touches a live owner)
|
|
638
748
|
|
|
639
749
|
${chalk.bold("Examples:")}
|
|
@@ -642,8 +752,8 @@ ${chalk.bold("Examples:")}
|
|
|
642
752
|
${APP_NAME} notify setup discord --discord-bot-token <token> --discord-application-id <id> --discord-guild-id <id> --discord-parent-channel-id <id>
|
|
643
753
|
${APP_NAME} notify setup slack --slack-bot-token <token> --slack-app-token <token> --slack-workspace-id <id> --slack-channel-id <id> [--slack-authorized-user-id <id>]
|
|
644
754
|
${APP_NAME} notify status
|
|
645
|
-
${APP_NAME} notify health --probe
|
|
646
|
-
${APP_NAME} notify test --message "hello from gjc"
|
|
755
|
+
${APP_NAME} notify health --provider discord --probe
|
|
756
|
+
${APP_NAME} notify test --provider slack --message "hello from gjc"
|
|
647
757
|
${APP_NAME} notify recovery
|
|
648
758
|
|
|
649
759
|
${chalk.bold("Threaded Mode:")}
|
package/src/commands/notify.ts
CHANGED
|
@@ -40,7 +40,11 @@ export default class Notify extends Command {
|
|
|
40
40
|
description: "Slack user id authorized for inbound replies and commands",
|
|
41
41
|
}),
|
|
42
42
|
redact: Flags.boolean({ description: "Enable redaction of remote notification content" }),
|
|
43
|
-
|
|
43
|
+
provider: Flags.string({
|
|
44
|
+
description: "notify health/test: select telegram, discord, or slack",
|
|
45
|
+
options: ["telegram", "discord", "slack"],
|
|
46
|
+
}),
|
|
47
|
+
probe: Flags.boolean({ description: "notify health: run the selected provider's REST diagnostic" }),
|
|
44
48
|
message: Flags.string({ description: "notify test: custom message body" }),
|
|
45
49
|
"owner-id": Flags.string({ description: "Internal: daemon owner id" }),
|
|
46
50
|
"agent-dir": Flags.string({ description: "Internal: agent dir for the daemon" }),
|
|
@@ -64,16 +68,23 @@ export default class Notify extends Command {
|
|
|
64
68
|
...(agentDir ? ["--agent-dir", agentDir] : []),
|
|
65
69
|
...extra,
|
|
66
70
|
];
|
|
67
|
-
const
|
|
71
|
+
const positionalProvider = action === "setup" ? extra[0] : undefined;
|
|
68
72
|
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
provider !== "slack"
|
|
73
|
+
positionalProvider !== undefined &&
|
|
74
|
+
positionalProvider !== "telegram" &&
|
|
75
|
+
positionalProvider !== "discord" &&
|
|
76
|
+
positionalProvider !== "slack"
|
|
74
77
|
) {
|
|
75
|
-
throw new Error(`Unknown notification provider: ${
|
|
78
|
+
throw new Error(`Unknown notification provider: ${positionalProvider}`);
|
|
79
|
+
}
|
|
80
|
+
const providerFlag = flagRec.provider as string | undefined;
|
|
81
|
+
if (providerFlag && action !== "health" && action !== "test") {
|
|
82
|
+
throw new Error("--provider is valid only for notify health and notify test.");
|
|
83
|
+
}
|
|
84
|
+
if (action !== "setup" && action !== "daemon-internal" && extra.length > 0) {
|
|
85
|
+
throw new Error(`Unexpected notify arguments: ${extra.join(" ")}`);
|
|
76
86
|
}
|
|
87
|
+
const provider = providerFlag ?? positionalProvider;
|
|
77
88
|
|
|
78
89
|
const cmd: NotifyCommandArgs = {
|
|
79
90
|
action: action as NotifyAction,
|
package/src/config/file-lock.ts
CHANGED
|
@@ -17,6 +17,10 @@ const DEFAULT_OPTIONS: Required<FileLockOptions> = {
|
|
|
17
17
|
|
|
18
18
|
type LockInfo = FileLockOwnerToken;
|
|
19
19
|
|
|
20
|
+
export const FileLockTestHooks: {
|
|
21
|
+
afterParentMkdir?: (lockPath: string) => void | Promise<void>;
|
|
22
|
+
} = {};
|
|
23
|
+
|
|
20
24
|
/**
|
|
21
25
|
* Returns the OS-provided process start timestamp for PID-reuse detection.
|
|
22
26
|
* `ps` is available on the supported Unix hosts (macOS and Linux), unlike
|
|
@@ -237,6 +241,8 @@ async function removeStaleLockForAcquire(lockPath: string, snapshot: LockStaleSn
|
|
|
237
241
|
|
|
238
242
|
async function tryAcquireLock(lockPath: string): Promise<LockInfo | null> {
|
|
239
243
|
await fs.mkdir(path.dirname(lockPath), { recursive: true });
|
|
244
|
+
const afterParentMkdir = FileLockTestHooks.afterParentMkdir;
|
|
245
|
+
if (afterParentMkdir) await afterParentMkdir(lockPath);
|
|
240
246
|
try {
|
|
241
247
|
await fs.mkdir(lockPath);
|
|
242
248
|
return await writeLockInfo(lockPath);
|
|
@@ -37,6 +37,8 @@ export interface DiscoveryMergeInput {
|
|
|
37
37
|
models: readonly Model<Api>[];
|
|
38
38
|
state: ProviderDiscoveryState;
|
|
39
39
|
warning?: string;
|
|
40
|
+
authGeneration?: string;
|
|
41
|
+
fetched?: boolean;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export interface ProviderDiscoveryCallbacks<TProvider extends DiscoveryProvider> {
|
|
@@ -44,7 +46,9 @@ export interface ProviderDiscoveryCallbacks<TProvider extends DiscoveryProvider>
|
|
|
44
46
|
requiresAuth: (provider: TProvider) => boolean;
|
|
45
47
|
peekApiKey: (provider: TProvider) => Promise<string | undefined>;
|
|
46
48
|
isAuthenticated: (apiKey: string | undefined) => boolean;
|
|
47
|
-
fetchModels: (provider: TProvider) => Promise<Model<Api>[]>;
|
|
49
|
+
fetchModels: (provider: TProvider, apiKey: string | undefined) => Promise<Model<Api>[]>;
|
|
50
|
+
getEvidenceGeneration?: (provider: TProvider) => string;
|
|
51
|
+
canPublishCache?: (provider: TProvider) => boolean;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/** Owns configured discovery inputs, status, cache lifecycle, and refresh generations. */
|
|
@@ -89,6 +93,11 @@ export class ModelDiscoveryManager<TProvider extends DiscoveryProvider> {
|
|
|
89
93
|
const state = this.#states.get(provider);
|
|
90
94
|
return state === undefined ? undefined : this.#snapshot(state);
|
|
91
95
|
}
|
|
96
|
+
invalidate(provider: string): void {
|
|
97
|
+
this.#invalidate(provider);
|
|
98
|
+
this.#states.delete(provider);
|
|
99
|
+
this.#lastWarnings.delete(provider);
|
|
100
|
+
}
|
|
92
101
|
|
|
93
102
|
loadCached(provider: TProvider, cacheDbPath?: string): readonly Model<Api>[] {
|
|
94
103
|
const cache = readModelCache<Api>(provider.provider, 24 * 60 * 60 * 1000, Date.now, cacheDbPath);
|
|
@@ -133,9 +142,21 @@ export class ModelDiscoveryManager<TProvider extends DiscoveryProvider> {
|
|
|
133
142
|
models: models.map(model => model.id),
|
|
134
143
|
});
|
|
135
144
|
|
|
145
|
+
let authGeneration = callbacks.getEvidenceGeneration?.(provider);
|
|
146
|
+
let apiKey: string | undefined;
|
|
136
147
|
if (callbacks.requiresAuth(provider)) {
|
|
137
|
-
|
|
148
|
+
apiKey = await callbacks.peekApiKey(provider);
|
|
149
|
+
const resolvedGeneration = callbacks.getEvidenceGeneration?.(provider);
|
|
138
150
|
if (!this.isCurrent(token)) return this.#stale(token);
|
|
151
|
+
if (authGeneration !== resolvedGeneration) {
|
|
152
|
+
authGeneration = resolvedGeneration;
|
|
153
|
+
// Resolving a command-backed key can update the evidence generation. Keep
|
|
154
|
+
// the key resolved for this refresh so round-robin selection cannot switch
|
|
155
|
+
// credentials between the request and its published evidence.
|
|
156
|
+
if (!this.isCurrent(token) || authGeneration !== callbacks.getEvidenceGeneration?.(provider)) {
|
|
157
|
+
return this.#stale(token);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
139
160
|
if (!callbacks.isAuthenticated(apiKey)) return unauthenticated(cachedModels);
|
|
140
161
|
}
|
|
141
162
|
|
|
@@ -145,10 +166,14 @@ export class ModelDiscoveryManager<TProvider extends DiscoveryProvider> {
|
|
|
145
166
|
staticModels: [],
|
|
146
167
|
cacheDbPath: callbacks.cacheDbPath,
|
|
147
168
|
cacheTtlMs: 24 * 60 * 60 * 1000,
|
|
148
|
-
canPublishCache: () =>
|
|
169
|
+
canPublishCache: () =>
|
|
170
|
+
this.isCurrent(token) &&
|
|
171
|
+
(callbacks.getEvidenceGeneration === undefined ||
|
|
172
|
+
callbacks.getEvidenceGeneration(provider) === authGeneration) &&
|
|
173
|
+
(callbacks.canPublishCache?.(provider) ?? true),
|
|
149
174
|
fetchDynamicModels: async () => {
|
|
150
175
|
try {
|
|
151
|
-
return await callbacks.fetchModels(provider);
|
|
176
|
+
return await callbacks.fetchModels(provider, apiKey);
|
|
152
177
|
} catch (cause) {
|
|
153
178
|
error = cause instanceof Error ? cause.message : String(cause);
|
|
154
179
|
return null;
|
|
@@ -156,7 +181,11 @@ export class ModelDiscoveryManager<TProvider extends DiscoveryProvider> {
|
|
|
156
181
|
},
|
|
157
182
|
});
|
|
158
183
|
const result = await manager.refresh(strategy);
|
|
159
|
-
if (
|
|
184
|
+
if (
|
|
185
|
+
!this.isCurrent(token) ||
|
|
186
|
+
(callbacks.getEvidenceGeneration !== undefined && callbacks.getEvidenceGeneration(provider) !== authGeneration)
|
|
187
|
+
)
|
|
188
|
+
return this.#stale(token);
|
|
160
189
|
const status: ProviderDiscoveryStatus = error
|
|
161
190
|
? result.models.length > 0
|
|
162
191
|
? "cached"
|
|
@@ -166,7 +195,9 @@ export class ModelDiscoveryManager<TProvider extends DiscoveryProvider> {
|
|
|
166
195
|
? "cached"
|
|
167
196
|
: "idle"
|
|
168
197
|
: result.models.length > 0
|
|
169
|
-
?
|
|
198
|
+
? result.stale
|
|
199
|
+
? "cached"
|
|
200
|
+
: "ok"
|
|
170
201
|
: "empty";
|
|
171
202
|
const state: ProviderDiscoveryState = {
|
|
172
203
|
provider: provider.provider,
|
|
@@ -177,7 +208,7 @@ export class ModelDiscoveryManager<TProvider extends DiscoveryProvider> {
|
|
|
177
208
|
models: result.models.map(model => model.id),
|
|
178
209
|
error,
|
|
179
210
|
};
|
|
180
|
-
return this.#complete(token, result.models, state, error);
|
|
211
|
+
return this.#complete(token, result.models, state, error, authGeneration, result.fetched);
|
|
181
212
|
}
|
|
182
213
|
|
|
183
214
|
#complete(
|
|
@@ -185,6 +216,8 @@ export class ModelDiscoveryManager<TProvider extends DiscoveryProvider> {
|
|
|
185
216
|
models: readonly Model<Api>[],
|
|
186
217
|
state: ProviderDiscoveryState,
|
|
187
218
|
error?: string,
|
|
219
|
+
authGeneration?: string,
|
|
220
|
+
fetched?: boolean,
|
|
188
221
|
): DiscoveryMergeInput {
|
|
189
222
|
const current = this.isCurrent(token);
|
|
190
223
|
if (current) this.#states.set(token.provider, this.#snapshot(state));
|
|
@@ -193,7 +226,16 @@ export class ModelDiscoveryManager<TProvider extends DiscoveryProvider> {
|
|
|
193
226
|
if (error) this.#lastWarnings.set(token.provider, error);
|
|
194
227
|
else this.#lastWarnings.delete(token.provider);
|
|
195
228
|
}
|
|
196
|
-
return this.#snapshot({
|
|
229
|
+
return this.#snapshot({
|
|
230
|
+
provider: token.provider,
|
|
231
|
+
token,
|
|
232
|
+
current,
|
|
233
|
+
models,
|
|
234
|
+
state,
|
|
235
|
+
warning,
|
|
236
|
+
authGeneration,
|
|
237
|
+
fetched,
|
|
238
|
+
});
|
|
197
239
|
}
|
|
198
240
|
|
|
199
241
|
#stale(token: DiscoveryRefreshToken): DiscoveryMergeInput {
|