@gajae-code/coding-agent 0.12.7 → 0.12.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/commands/sdk.d.ts +23 -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/discovery/agents-md.d.ts +11 -1
- 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/modes/types.d.ts +6 -0
- 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 -1
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- 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 +54 -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 +8 -1
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- 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/internal/managed-session-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +19 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +3 -2
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/session-manager.d.ts +3 -1
- package/dist/types/session/session-storage.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +12 -1
- package/dist/types/tools/composer-bash-policy.d.ts +9 -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/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
- package/src/cli/notify-cli.ts +215 -99
- package/src/commands/notify.ts +19 -8
- package/src/commands/sdk.ts +54 -0
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-profiles.ts +8 -0
- package/src/config/model-registry.ts +866 -76
- 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/discovery/agents-md.ts +76 -19
- package/src/gjc-runtime/psmux-detect.ts +12 -16
- package/src/gjc-runtime/session-state-sidecar.ts +12 -2
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/internal-urls/docs-index.generated.ts +10 -9
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/components/extensions/inspector-panel.ts +9 -5
- 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 +525 -93
- package/src/modes/print-mode.ts +3 -0
- package/src/modes/types.ts +6 -0
- package/src/prompts/system/system-prompt.md +1 -0
- 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 -6
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/daemon-paths.ts +2 -0
- 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 +97 -35
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +571 -137
- 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 +8 -1
- 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/query/handlers.ts +20 -1
- 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 +34 -14
- package/src/session/agent-session.ts +109 -37
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +418 -47
- package/src/session/internal/native-publish-outcome.ts +15 -3
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/session-manager.ts +27 -3
- package/src/session/session-storage.ts +72 -5
- package/src/slash-commands/builtin-registry.ts +117 -15
- package/src/system-prompt.ts +28 -15
- package/src/task/index.ts +9 -3
- package/src/tools/ask.ts +1 -1
- package/src/tools/bash.ts +4 -2
- package/src/tools/composer-bash-policy.ts +21 -5
- package/src/tools/telegram-send.ts +4 -2
- package/src/utils/command-args.ts +19 -30
- package/src/utils/lang-from-path.ts +2 -4
|
@@ -9,6 +9,7 @@ const packageName = "@gajae-code/coding-agent";
|
|
|
9
9
|
const aiPackageDir = path.resolve(packageDir, "../ai");
|
|
10
10
|
const bridgeClientPackageDir = path.resolve(packageDir, "../bridge-client");
|
|
11
11
|
const tuiPackageDir = path.resolve(packageDir, "../tui");
|
|
12
|
+
const agentPackageDir = path.resolve(packageDir, "../agent");
|
|
12
13
|
const nativesPackageDir = path.resolve(packageDir, "../natives");
|
|
13
14
|
const linuxX64PackageDir = path.resolve(packageDir, "../natives-linux-x64");
|
|
14
15
|
const manifestsDir = path.join(packageDir, "test/manifests");
|
|
@@ -41,6 +42,7 @@ async function runSmoke(): Promise<Surface> {
|
|
|
41
42
|
await fs.copyFile(path.join(nativesPackageDir, "native", entry), path.join(stagedNativeDir, entry));
|
|
42
43
|
}
|
|
43
44
|
}
|
|
45
|
+
const agentTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], agentPackageDir);
|
|
44
46
|
const aiTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], aiPackageDir);
|
|
45
47
|
const bridgeClientTarball = run(
|
|
46
48
|
["bun", "pm", "pack", "--destination", tempDir, "--quiet"],
|
|
@@ -49,6 +51,7 @@ async function runSmoke(): Promise<Surface> {
|
|
|
49
51
|
const tuiTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], tuiPackageDir);
|
|
50
52
|
const nativesTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], nativesPackageDir);
|
|
51
53
|
const linuxX64Tarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], stagedLinuxX64Dir);
|
|
54
|
+
const agentTarballPath = path.isAbsolute(agentTarball) ? agentTarball : path.join(agentPackageDir, agentTarball);
|
|
52
55
|
const codingAgentTarball = run(["bun", "pm", "pack", "--destination", tempDir, "--quiet"], packageDir);
|
|
53
56
|
const aiTarballPath = path.isAbsolute(aiTarball) ? aiTarball : path.join(aiPackageDir, aiTarball);
|
|
54
57
|
const bridgeClientTarballPath = path.isAbsolute(bridgeClientTarball)
|
|
@@ -71,6 +74,7 @@ async function runSmoke(): Promise<Surface> {
|
|
|
71
74
|
name: "sdk-smoke",
|
|
72
75
|
private: true,
|
|
73
76
|
dependencies: {
|
|
77
|
+
"@gajae-code/agent-core": `file:${agentTarballPath}`,
|
|
74
78
|
"@gajae-code/ai": `file:${aiTarballPath}`,
|
|
75
79
|
"@gajae-code/bridge-client": `file:${bridgeClientTarballPath}`,
|
|
76
80
|
[packageName]: `file:${codingAgentTarballPath}`,
|
|
@@ -79,6 +83,7 @@ async function runSmoke(): Promise<Surface> {
|
|
|
79
83
|
"@gajae-code/natives-linux-x64": `file:${linuxX64TarballPath}`,
|
|
80
84
|
},
|
|
81
85
|
overrides: {
|
|
86
|
+
"@gajae-code/agent-core": `file:${agentTarballPath}`,
|
|
82
87
|
"@gajae-code/ai": `file:${aiTarballPath}`,
|
|
83
88
|
"@gajae-code/bridge-client": `file:${bridgeClientTarballPath}`,
|
|
84
89
|
"@gajae-code/tui": `file:${tuiTarballPath}`,
|
|
@@ -96,6 +101,52 @@ async function runSmoke(): Promise<Surface> {
|
|
|
96
101
|
const installedPackage = JSON.parse(
|
|
97
102
|
await fs.readFile(path.join(tempDir, "node_modules", packageName, "package.json"), "utf8"),
|
|
98
103
|
) as { exports?: Record<string, unknown> };
|
|
104
|
+
const installedAgentPackagePath = path.join(tempDir, "node_modules", "@gajae-code", "agent-core");
|
|
105
|
+
const installedAgentPackageJsonPath = path.join(installedAgentPackagePath, "package.json");
|
|
106
|
+
const installedAgentPackage = JSON.parse(await fs.readFile(installedAgentPackageJsonPath, "utf8")) as {
|
|
107
|
+
name?: string;
|
|
108
|
+
version?: string;
|
|
109
|
+
};
|
|
110
|
+
const expectedAgentPackage = JSON.parse(
|
|
111
|
+
await fs.readFile(path.join(agentPackageDir, "package.json"), "utf8"),
|
|
112
|
+
) as {
|
|
113
|
+
name?: string;
|
|
114
|
+
version?: string;
|
|
115
|
+
};
|
|
116
|
+
const installedAgentRealpath = await fs.realpath(installedAgentPackagePath);
|
|
117
|
+
const tempRealpath = await fs.realpath(tempDir);
|
|
118
|
+
const sourceAgentRealpath = await fs.realpath(agentPackageDir);
|
|
119
|
+
if (
|
|
120
|
+
!installedAgentRealpath.startsWith(`${tempRealpath}${path.sep}`) ||
|
|
121
|
+
installedAgentRealpath.startsWith(`${sourceAgentRealpath}${path.sep}`)
|
|
122
|
+
) {
|
|
123
|
+
throw new Error("packed smoke resolved agent-core outside the temporary packed installation");
|
|
124
|
+
}
|
|
125
|
+
const packedAgentInspectDir = path.join(tempDir, "packed-agent-inspect");
|
|
126
|
+
await fs.mkdir(packedAgentInspectDir);
|
|
127
|
+
const extract = Bun.spawnSync(["tar", "xzf", agentTarballPath, "-C", packedAgentInspectDir], {
|
|
128
|
+
stdout: "pipe",
|
|
129
|
+
stderr: "pipe",
|
|
130
|
+
});
|
|
131
|
+
if (extract.exitCode !== 0)
|
|
132
|
+
throw new Error(`could not inspect packed agent-core tarball: ${extract.stderr.toString()}`);
|
|
133
|
+
const packedAgentPackageJson = await fs.readFile(
|
|
134
|
+
path.join(packedAgentInspectDir, "package", "package.json"),
|
|
135
|
+
"utf8",
|
|
136
|
+
);
|
|
137
|
+
const installedAgentPackageJson = await fs.readFile(installedAgentPackageJsonPath, "utf8");
|
|
138
|
+
if (installedAgentPackageJson !== packedAgentPackageJson) {
|
|
139
|
+
throw new Error("packed smoke installed agent-core content different from the packed tarball");
|
|
140
|
+
}
|
|
141
|
+
if (
|
|
142
|
+
installedAgentPackage.name !== expectedAgentPackage.name ||
|
|
143
|
+
installedAgentPackage.version !== expectedAgentPackage.version
|
|
144
|
+
) {
|
|
145
|
+
throw new Error("packed smoke installed a mismatched agent-core package");
|
|
146
|
+
}
|
|
147
|
+
if (installedAgentPackage.name !== "@gajae-code/agent-core") {
|
|
148
|
+
throw new Error("packed smoke agent-core package identity is invalid");
|
|
149
|
+
}
|
|
99
150
|
if (installedPackage.exports?.["./session/internal/*"] !== null) {
|
|
100
151
|
throw new Error("packed package must explicitly block ./session/internal/*");
|
|
101
152
|
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "../test/fixtures/tui/notifications-settings-showcase";
|
|
12
12
|
|
|
13
13
|
const CANONICAL_COMMAND =
|
|
14
|
-
"bun packages/coding-agent/scripts/capture-notifications-settings-showcase.ts --output .gjc/qa/issue-
|
|
14
|
+
"bun packages/coding-agent/scripts/capture-notifications-settings-showcase.ts --output .gjc/qa/issue-3570-notifications";
|
|
15
15
|
const DETERMINISTIC_CAPTURE_TIMESTAMP = "1970-01-01T00:00:00.000Z";
|
|
16
16
|
const CAPTURE_TOOL_VERSION = "notifications-settings-showcase-live-settings-selector-v3";
|
|
17
17
|
|
|
@@ -1531,13 +1531,26 @@ function exactTeamRuntimeSendKeysRanges(contents: string): ShellRange[] {
|
|
|
1531
1531
|
return [];
|
|
1532
1532
|
|
|
1533
1533
|
const continuationBody = contents.slice(continuationRange.start, continuationRange.end);
|
|
1534
|
+
// The frozen argv may address the pane either through `worker.pane_id` directly or
|
|
1535
|
+
// through a local binding that was proven non-empty first (the optional field does
|
|
1536
|
+
// not narrow for the type checker). Either way both send operations must name the
|
|
1537
|
+
// identical pane token, and a local token must come from a checked `worker.pane_id`.
|
|
1534
1538
|
const continuationArgs =
|
|
1535
|
-
/const\s+args\s*=\s*Object\.freeze\(\s*\[\s*["']send-keys["']\s*,\s*["']-l["']\s*,\s*["']-t["']\s*,\s*worker\.pane_id\s*,\s*continuationPrompt\s*,\s*["'];["']\s*,\s*["']send-keys["']\s*,\s*["']-t["']\s*,\s*worker\.pane_id\s*,\s*["']Enter["']\s*,?\s*\]\s*\)\s*;/.exec(
|
|
1539
|
+
/const\s+args(?:\s*:\s*readonly\s+string\[\])?\s*=\s*Object\.freeze\(\s*\[\s*["']send-keys["']\s*,\s*["']-l["']\s*,\s*["']-t["']\s*,\s*(worker\.pane_id|[A-Za-z_$][\w$]*)\s*,\s*continuationPrompt\s*,\s*["'];["']\s*,\s*["']send-keys["']\s*,\s*["']-t["']\s*,\s*(worker\.pane_id|[A-Za-z_$][\w$]*)\s*,\s*["']Enter["']\s*,?\s*\]\s*\)\s*;/.exec(
|
|
1536
1540
|
continuationBody,
|
|
1537
1541
|
);
|
|
1542
|
+
const paneToken = continuationArgs?.[1];
|
|
1543
|
+
const paneTokenIsProvenLocal =
|
|
1544
|
+
paneToken !== undefined &&
|
|
1545
|
+
paneToken !== "worker.pane_id" &&
|
|
1546
|
+
new RegExp(`const\\s+${paneToken}\\s*=\\s*worker\\.pane_id\\s*;`).test(continuationBody) &&
|
|
1547
|
+
new RegExp(`if\\s*\\(\\s*!${paneToken}\\s*\\)\\s*return\\b`).test(continuationBody);
|
|
1538
1548
|
if (
|
|
1539
1549
|
!continuationArgs ||
|
|
1550
|
+
continuationArgs[1] !== continuationArgs[2] ||
|
|
1551
|
+
!(paneToken === "worker.pane_id" || paneTokenIsProvenLocal) ||
|
|
1540
1552
|
!continuationBody.includes("const revalidationReason =") ||
|
|
1553
|
+
!/if\s*\(\s*revalidationReason\s*\)\s*(?:\{\s*return\b[^}]*;?\s*\}|return\b[^;]*;)/.test(continuationBody) ||
|
|
1541
1554
|
!continuationBody.includes("await createJsonNoClobber(") ||
|
|
1542
1555
|
!continuationBody.includes('type: "literal-send"') ||
|
|
1543
1556
|
!continuationBody.includes('type: "key-send"') ||
|
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,
|
|
@@ -61,6 +69,7 @@ export interface NotifyCommandArgs {
|
|
|
61
69
|
slackChannelId?: string;
|
|
62
70
|
slackAuthorizedUserId?: string;
|
|
63
71
|
redact?: boolean;
|
|
72
|
+
forceDaemonLock?: boolean;
|
|
64
73
|
probe?: boolean;
|
|
65
74
|
message?: string;
|
|
66
75
|
}
|
|
@@ -93,75 +102,104 @@ export interface NotifyCommandDeps {
|
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
|
|
96
|
-
if (args.length === 0 || args[0] !== "notify")
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
|
|
105
|
+
if (args.length === 0 || args[0] !== "notify") return undefined;
|
|
100
106
|
const action = args[1];
|
|
101
|
-
|
|
107
|
+
const providerValue = (value: string | undefined): NotifySetupProvider | undefined =>
|
|
108
|
+
value === "telegram" || value === "discord" || value === "slack" ? value : undefined;
|
|
109
|
+
const parseFlags = (
|
|
110
|
+
rest: string[],
|
|
111
|
+
valueFlags: ReadonlySet<string>,
|
|
112
|
+
booleanFlags: ReadonlySet<string>,
|
|
113
|
+
): Map<string, string | true> | undefined => {
|
|
114
|
+
const parsed = new Map<string, string | true>();
|
|
115
|
+
for (let index = 0; index < rest.length; index++) {
|
|
116
|
+
const flag = rest[index];
|
|
117
|
+
if (!flag?.startsWith("--") || parsed.has(flag)) return undefined;
|
|
118
|
+
if (booleanFlags.has(flag)) {
|
|
119
|
+
parsed.set(flag, true);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!valueFlags.has(flag)) return undefined;
|
|
123
|
+
const value = rest[++index];
|
|
124
|
+
if (!value || value.startsWith("--")) return undefined;
|
|
125
|
+
parsed.set(flag, value);
|
|
126
|
+
}
|
|
127
|
+
return parsed;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (action === "setup") {
|
|
102
131
|
const rest = args.slice(2);
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
132
|
+
const positional = rest[0]?.startsWith("--") ? undefined : rest.shift();
|
|
133
|
+
const provider = positional === undefined ? undefined : providerValue(positional);
|
|
134
|
+
if (positional !== undefined && !provider) return undefined;
|
|
135
|
+
const flags = parseFlags(
|
|
136
|
+
rest,
|
|
137
|
+
new Set([
|
|
138
|
+
"--token",
|
|
139
|
+
"--chat-id",
|
|
140
|
+
"--discord-bot-token",
|
|
141
|
+
"--discord-application-id",
|
|
142
|
+
"--discord-guild-id",
|
|
143
|
+
"--discord-parent-channel-id",
|
|
144
|
+
"--slack-bot-token",
|
|
145
|
+
"--slack-app-token",
|
|
146
|
+
"--slack-workspace-id",
|
|
147
|
+
"--slack-channel-id",
|
|
148
|
+
"--slack-authorized-user-id",
|
|
149
|
+
]),
|
|
150
|
+
new Set(["--redact"]),
|
|
151
|
+
);
|
|
152
|
+
if (!flags) return undefined;
|
|
153
|
+
const value = (name: string): string | undefined => {
|
|
154
|
+
const found = flags.get(name);
|
|
155
|
+
return typeof found === "string" ? found : undefined;
|
|
106
156
|
};
|
|
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
157
|
return {
|
|
133
158
|
action,
|
|
134
|
-
rawArgs:
|
|
159
|
+
rawArgs: args.slice(2),
|
|
135
160
|
...(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"),
|
|
161
|
+
token: value("--token"),
|
|
162
|
+
chatId: value("--chat-id"),
|
|
163
|
+
discordBotToken: value("--discord-bot-token"),
|
|
164
|
+
discordApplicationId: value("--discord-application-id"),
|
|
165
|
+
discordGuildId: value("--discord-guild-id"),
|
|
166
|
+
discordParentChannelId: value("--discord-parent-channel-id"),
|
|
167
|
+
slackBotToken: value("--slack-bot-token"),
|
|
168
|
+
slackAppToken: value("--slack-app-token"),
|
|
169
|
+
slackWorkspaceId: value("--slack-workspace-id"),
|
|
170
|
+
slackChannelId: value("--slack-channel-id"),
|
|
171
|
+
slackAuthorizedUserId: value("--slack-authorized-user-id"),
|
|
172
|
+
redact: flags.get("--redact") === true,
|
|
150
173
|
};
|
|
151
174
|
}
|
|
152
|
-
if (action === "
|
|
175
|
+
if (action === "status") {
|
|
176
|
+
return args.length === 2 ? { action, rawArgs: [] } : undefined;
|
|
177
|
+
}
|
|
178
|
+
if (action === "health" || action === "test") {
|
|
153
179
|
const rest = args.slice(2);
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
180
|
+
const flags = parseFlags(
|
|
181
|
+
rest,
|
|
182
|
+
new Set(action === "health" ? ["--provider"] : ["--provider", "--message"]),
|
|
183
|
+
new Set(action === "health" ? ["--probe"] : []),
|
|
184
|
+
);
|
|
185
|
+
if (!flags) return undefined;
|
|
186
|
+
const rawProvider = flags.get("--provider");
|
|
187
|
+
const provider = typeof rawProvider === "string" ? providerValue(rawProvider) : undefined;
|
|
188
|
+
if (rawProvider !== undefined && !provider) return undefined;
|
|
158
189
|
return {
|
|
159
190
|
action,
|
|
160
191
|
rawArgs: rest,
|
|
161
|
-
|
|
162
|
-
|
|
192
|
+
...(provider ? { provider } : {}),
|
|
193
|
+
probe: flags.get("--probe") === true,
|
|
194
|
+
message: typeof flags.get("--message") === "string" ? (flags.get("--message") as string) : undefined,
|
|
163
195
|
};
|
|
164
196
|
}
|
|
197
|
+
if (action === "recovery") {
|
|
198
|
+
const flags = parseFlags(args.slice(2), new Set(), new Set(["--force-daemon-lock"]));
|
|
199
|
+
return flags
|
|
200
|
+
? { action, rawArgs: args.slice(2), forceDaemonLock: flags.get("--force-daemon-lock") === true }
|
|
201
|
+
: undefined;
|
|
202
|
+
}
|
|
165
203
|
if (action === "daemon-internal") {
|
|
166
204
|
return {
|
|
167
205
|
action,
|
|
@@ -169,7 +207,6 @@ export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
|
|
|
169
207
|
rawArgs: args.slice(2),
|
|
170
208
|
};
|
|
171
209
|
}
|
|
172
|
-
|
|
173
210
|
return undefined;
|
|
174
211
|
}
|
|
175
212
|
|
|
@@ -193,7 +230,7 @@ export async function runNotifyCommand(cmd: NotifyCommandArgs, deps: NotifyComma
|
|
|
193
230
|
await runTest(deps, cmd);
|
|
194
231
|
return;
|
|
195
232
|
case "recovery":
|
|
196
|
-
await runRecovery(deps);
|
|
233
|
+
await runRecovery(deps, cmd.forceDaemonLock);
|
|
197
234
|
return;
|
|
198
235
|
case "daemon-internal":
|
|
199
236
|
if (cmd.smoke) {
|
|
@@ -273,18 +310,46 @@ async function runDiscordSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps):
|
|
|
273
310
|
deps,
|
|
274
311
|
);
|
|
275
312
|
const settings = await getSettings(deps);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
313
|
+
let activationFailure: string | undefined;
|
|
314
|
+
let activationOutcome: EnsureChatDaemonResult | undefined;
|
|
315
|
+
const runtime: NotificationProviderRuntimeAuthority = {
|
|
316
|
+
activate: async provider => {
|
|
317
|
+
if (provider !== "discord") throw new Error("Unexpected provider activation request.");
|
|
318
|
+
try {
|
|
319
|
+
const result = await ensureConfiguredProviderDaemon("discord", settings, deps);
|
|
320
|
+
if (result === "disabled") throw new Error("Discord runtime did not activate.");
|
|
321
|
+
activationOutcome = result;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
activationFailure = error instanceof Error ? error.message : "Discord runtime activation failed.";
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
deactivate: async () => undefined,
|
|
328
|
+
};
|
|
329
|
+
const result = await mutateNotificationProvider({
|
|
330
|
+
settings,
|
|
331
|
+
mutation: {
|
|
332
|
+
provider: "discord",
|
|
333
|
+
botToken: { action: "replace", value: botToken },
|
|
334
|
+
applicationId,
|
|
335
|
+
guildId,
|
|
336
|
+
parentChannelId,
|
|
337
|
+
},
|
|
338
|
+
configureAndActivate: true,
|
|
339
|
+
...(cmd.redact ? { redact: true } : {}),
|
|
340
|
+
runtime,
|
|
341
|
+
});
|
|
342
|
+
if (result.status === "commit_failed")
|
|
343
|
+
throw new Error("Discord configuration was not saved because the CAS commit failed.");
|
|
344
|
+
if (result.status !== "activated") {
|
|
345
|
+
const detail = `runtime activation failed: ${activationFailure ?? result.status}`;
|
|
346
|
+
process.stderr.write(`Discord configuration saved, but ${detail}.\n`);
|
|
347
|
+
if (deps.setExitCode) deps.setExitCode(1);
|
|
348
|
+
else process.exitCode = 1;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
286
351
|
process.stdout.write(
|
|
287
|
-
`Discord
|
|
352
|
+
`Discord configuration saved and activated. botToken=${maskToken(botToken)} applicationId=${applicationId} guildId=${guildId} parentChannelId=${parentChannelId} daemon=${activationOutcome ?? "attached"}\n`,
|
|
288
353
|
);
|
|
289
354
|
}
|
|
290
355
|
|
|
@@ -295,21 +360,47 @@ async function runSlackSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps): P
|
|
|
295
360
|
const channelId = await promptSetupValue(cmd.slackChannelId, "--slack-channel-id", false, deps);
|
|
296
361
|
const authorizedUserId = cmd.slackAuthorizedUserId?.trim() || undefined;
|
|
297
362
|
const settings = await getSettings(deps);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
363
|
+
let activationFailure: string | undefined;
|
|
364
|
+
let activationOutcome: EnsureChatDaemonResult | undefined;
|
|
365
|
+
const runtime: NotificationProviderRuntimeAuthority = {
|
|
366
|
+
activate: async provider => {
|
|
367
|
+
if (provider !== "slack") throw new Error("Unexpected provider activation request.");
|
|
368
|
+
try {
|
|
369
|
+
const result = await ensureConfiguredProviderDaemon("slack", settings, deps);
|
|
370
|
+
if (result === "disabled") throw new Error("Slack runtime did not activate.");
|
|
371
|
+
activationOutcome = result;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
activationFailure = error instanceof Error ? error.message : "Slack runtime activation failed.";
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
deactivate: async () => undefined,
|
|
378
|
+
};
|
|
379
|
+
const result = await mutateNotificationProvider({
|
|
380
|
+
settings,
|
|
381
|
+
mutation: {
|
|
382
|
+
provider: "slack",
|
|
383
|
+
botToken: { action: "replace", value: botToken },
|
|
384
|
+
appToken: { action: "replace", value: appToken },
|
|
385
|
+
workspaceId,
|
|
386
|
+
channelId,
|
|
387
|
+
authorizedUserId,
|
|
388
|
+
},
|
|
389
|
+
configureAndActivate: true,
|
|
390
|
+
...(cmd.redact ? { redact: true } : {}),
|
|
391
|
+
runtime,
|
|
392
|
+
});
|
|
393
|
+
if (result.status === "commit_failed")
|
|
394
|
+
throw new Error("Slack configuration was not saved because the CAS commit failed.");
|
|
395
|
+
if (result.status !== "activated") {
|
|
396
|
+
const detail = `runtime activation failed: ${activationFailure ?? result.status}`;
|
|
397
|
+
process.stderr.write(`Slack configuration saved, but ${detail}.\n`);
|
|
398
|
+
if (deps.setExitCode) deps.setExitCode(1);
|
|
399
|
+
else process.exitCode = 1;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
311
402
|
process.stdout.write(
|
|
312
|
-
`Slack
|
|
403
|
+
`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
404
|
);
|
|
314
405
|
}
|
|
315
406
|
|
|
@@ -365,6 +456,7 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
365
456
|
if (result.pairingSource === "provided") {
|
|
366
457
|
process.stdout.write(`Using provided chat id ${result.chatId} (non-interactive).\n`);
|
|
367
458
|
}
|
|
459
|
+
let settingsCommitted = false;
|
|
368
460
|
try {
|
|
369
461
|
const proposedIdentity = deps.setupPreflight
|
|
370
462
|
? proposedIdentityFromSetupPreflight(deps.setupPreflight, token.trim(), result.chatId)
|
|
@@ -385,9 +477,11 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
385
477
|
{ path: "notifications.telegram.botToken", op: "set", value: token.trim() },
|
|
386
478
|
{ path: "notifications.telegram.chatId", op: "set", value: result.chatId },
|
|
387
479
|
{ path: "notifications.enabled", op: "set", value: true },
|
|
480
|
+
{ path: "notifications.telegram.enabled", op: "set", value: true },
|
|
388
481
|
];
|
|
389
482
|
if (deps.setupRedact ?? cmd.redact) patches.push({ path: "notifications.redact", op: "set", value: true });
|
|
390
483
|
const receipt = await settings.commitAtomicBatch(patches);
|
|
484
|
+
settingsCommitted = true;
|
|
391
485
|
const activationMarker = createTelegramActivationMarker({
|
|
392
486
|
botToken: token.trim(),
|
|
393
487
|
chatId: result.chatId,
|
|
@@ -422,6 +516,7 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
422
516
|
});
|
|
423
517
|
if (activation.status === "blocked_identity") {
|
|
424
518
|
const restored = await activation.restore();
|
|
519
|
+
if (restored.status === "restored" || restored.status === "still_blocked") settingsCommitted = false;
|
|
425
520
|
const detail =
|
|
426
521
|
restored.status === "restored"
|
|
427
522
|
? "Telegram activation was blocked by a foreign daemon; previous settings were restored."
|
|
@@ -432,9 +527,18 @@ async function runTelegramSetup(cmd: NotifyCommandArgs, deps: NotifyCommandDeps)
|
|
|
432
527
|
: "Telegram activation was blocked; refusing to report setup success.";
|
|
433
528
|
throw new Error(detail);
|
|
434
529
|
}
|
|
530
|
+
if (activation.status === "activation_failed") {
|
|
531
|
+
receipt.discard();
|
|
532
|
+
throw new Error(activation.message);
|
|
533
|
+
}
|
|
534
|
+
receipt.discard();
|
|
435
535
|
} catch (error) {
|
|
436
536
|
const detail = sanitizeDiagnostic(error instanceof Error ? error.message : "unknown persistence failure", token);
|
|
437
|
-
throw new Error(
|
|
537
|
+
throw new Error(
|
|
538
|
+
settingsCommitted
|
|
539
|
+
? `Telegram notification settings were saved, but activation or recovery failed: ${detail}`
|
|
540
|
+
: `Unable to persist and activate Telegram notification settings: ${detail}`,
|
|
541
|
+
);
|
|
438
542
|
}
|
|
439
543
|
process.stdout.write(
|
|
440
544
|
`Notifications enabled. botToken=${maskToken(token)} chatId=${result.chatId} threaded=${result.threadedLabel}\n`,
|
|
@@ -586,6 +690,7 @@ async function runHealth(deps: NotifyCommandDeps, cmd: NotifyCommandArgs): Promi
|
|
|
586
690
|
const settings = await getSettings(deps);
|
|
587
691
|
const report = await checkNotificationHealth({
|
|
588
692
|
settings,
|
|
693
|
+
provider: cmd.provider,
|
|
589
694
|
probe: cmd.probe,
|
|
590
695
|
deps: { fetchImpl: deps.fetchImpl, apiBase: deps.apiBase },
|
|
591
696
|
});
|
|
@@ -598,17 +703,28 @@ async function runTest(deps: NotifyCommandDeps, cmd: NotifyCommandArgs): Promise
|
|
|
598
703
|
const settings = await getSettings(deps);
|
|
599
704
|
const result = await sendNotificationTest({
|
|
600
705
|
settings,
|
|
706
|
+
provider: cmd.provider,
|
|
601
707
|
text: cmd.message,
|
|
602
|
-
deps: {
|
|
708
|
+
deps: {
|
|
709
|
+
fetchImpl: deps.fetchImpl,
|
|
710
|
+
apiBase: deps.apiBase,
|
|
711
|
+
providerRuntimeStatus: async provider => {
|
|
712
|
+
const status =
|
|
713
|
+
provider === "telegram"
|
|
714
|
+
? await new TelegramDaemonController(settings).status()
|
|
715
|
+
: await new ChatDaemonController(settings, provider).status();
|
|
716
|
+
return status.health === "running" ? "ready" : "inactive";
|
|
717
|
+
},
|
|
718
|
+
},
|
|
603
719
|
});
|
|
604
720
|
process.stdout.write(`${formatNotificationTestResult(result)}\n`);
|
|
605
721
|
if (!result.ok && deps.setExitCode) deps.setExitCode(1);
|
|
606
722
|
else if (!result.ok) process.exitCode = 1;
|
|
607
723
|
}
|
|
608
724
|
|
|
609
|
-
async function runRecovery(deps: NotifyCommandDeps): Promise<void> {
|
|
725
|
+
async function runRecovery(deps: NotifyCommandDeps, forceDaemonLock = false): Promise<void> {
|
|
610
726
|
const settings = await getSettings(deps);
|
|
611
|
-
const report = await recoverNotifications({ settings });
|
|
727
|
+
const report = await recoverNotifications({ settings, forceDaemonLock });
|
|
612
728
|
process.stdout.write(`${formatNotificationRecoveryReport(report)}\n`);
|
|
613
729
|
}
|
|
614
730
|
|
|
@@ -616,8 +732,8 @@ export function printNotifyHelp(): void {
|
|
|
616
732
|
process.stdout.write(`${chalk.bold(`${APP_NAME} notify`)} - Configure Telegram, Discord, or Slack notifications
|
|
617
733
|
|
|
618
734
|
${chalk.bold("Interactive path:")}
|
|
619
|
-
In a running GJC session, use /settings → Notifications for
|
|
620
|
-
|
|
735
|
+
In a running GJC session, use /settings → Notifications for first-class Telegram, Discord,
|
|
736
|
+
and Slack configure/edit/repair, desired intent, health, test, removal, global master, and session controls.
|
|
621
737
|
The CLI subcommands below remain the authoritative headless and automation fallback.
|
|
622
738
|
|
|
623
739
|
${chalk.bold("Usage:")}
|
|
@@ -625,16 +741,16 @@ ${chalk.bold("Usage:")}
|
|
|
625
741
|
${APP_NAME} notify setup discord --discord-bot-token <token> --discord-application-id <id> --discord-guild-id <id> --discord-parent-channel-id <id>
|
|
626
742
|
${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
743
|
${APP_NAME} notify status
|
|
628
|
-
${APP_NAME} notify health [--probe]
|
|
629
|
-
${APP_NAME} notify test [--message <text>]
|
|
630
|
-
${APP_NAME} notify recovery
|
|
744
|
+
${APP_NAME} notify health [--provider telegram|discord|slack] [--probe]
|
|
745
|
+
${APP_NAME} notify test [--provider telegram|discord|slack] [--message <text>]
|
|
746
|
+
${APP_NAME} notify recovery [--force-daemon-lock]
|
|
631
747
|
|
|
632
748
|
${chalk.bold("Subcommands:")}
|
|
633
|
-
setup Pair Telegram or save complete
|
|
634
|
-
status Show
|
|
635
|
-
health Report
|
|
636
|
-
test Send a one-off test
|
|
637
|
-
recovery Clear dead-owner daemon locks and stale per-session endpoint files (never touches a live owner)
|
|
749
|
+
setup Pair Telegram or atomically save and activate complete Discord/Slack settings
|
|
750
|
+
status Show global master and provider configured/repair/desired/effective state without secrets
|
|
751
|
+
health Report selected provider state; --probe uses REST only and never opens Gateway/Socket Mode
|
|
752
|
+
test Send a one-off test through one selected or uniquely effective provider
|
|
753
|
+
recovery Clear dead-owner daemon locks and stale per-session endpoint files (never touches a live owner); --force-daemon-lock retries only with the same fail-closed dead-owner proof
|
|
638
754
|
|
|
639
755
|
${chalk.bold("Examples:")}
|
|
640
756
|
${APP_NAME} notify setup
|
|
@@ -642,8 +758,8 @@ ${chalk.bold("Examples:")}
|
|
|
642
758
|
${APP_NAME} notify setup discord --discord-bot-token <token> --discord-application-id <id> --discord-guild-id <id> --discord-parent-channel-id <id>
|
|
643
759
|
${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
760
|
${APP_NAME} notify status
|
|
645
|
-
${APP_NAME} notify health --probe
|
|
646
|
-
${APP_NAME} notify test --message "hello from gjc"
|
|
761
|
+
${APP_NAME} notify health --provider discord --probe
|
|
762
|
+
${APP_NAME} notify test --provider slack --message "hello from gjc"
|
|
647
763
|
${APP_NAME} notify recovery
|
|
648
764
|
|
|
649
765
|
${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,
|