@gajae-code/coding-agent 0.6.5 → 0.7.1
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 +38 -0
- package/dist/types/async/job-manager.d.ts +3 -1
- package/dist/types/cli/daemon-cli.d.ts +25 -0
- package/dist/types/cli/notify-cli.d.ts +23 -0
- package/dist/types/cli/setup-cli.d.ts +20 -1
- package/dist/types/commands/daemon.d.ts +41 -0
- package/dist/types/commands/notify.d.ts +41 -0
- package/dist/types/config/model-profile-activation.d.ts +12 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +38 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/daemon/builtin.d.ts +20 -0
- package/dist/types/daemon/control-types.d.ts +57 -0
- package/dist/types/daemon/runtime.d.ts +25 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +2 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +2 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +14 -0
- package/dist/types/modes/components/oauth-selector.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +61 -0
- package/dist/types/notifications/helpers.d.ts +55 -0
- package/dist/types/notifications/html-format.d.ts +62 -0
- package/dist/types/notifications/index.d.ts +28 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
- package/dist/types/notifications/telegram-cli.d.ts +19 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
- package/dist/types/notifications/telegram-daemon.d.ts +276 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +58 -0
- package/dist/types/notifications/threaded-render.d.ts +66 -0
- package/dist/types/notifications/topic-registry.d.ts +67 -0
- package/dist/types/rlm/index.d.ts +12 -0
- package/dist/types/session/agent-session.d.ts +39 -2
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/setup/credential-auto-import.d.ts +63 -0
- package/dist/types/setup/credential-import.d.ts +3 -0
- package/dist/types/setup/host-plugin-setup.d.ts +39 -0
- package/dist/types/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/index.d.ts +18 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +3 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/notify-cli.ts +274 -0
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -3
- package/src/commands/daemon.ts +47 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- package/src/config/model-profile-activation.ts +74 -5
- package/src/config/model-profiles.ts +7 -4
- package/src/config/model-registry.ts +6 -3
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +29 -0
- package/src/coordinator/contract.ts +3 -0
- package/src/coordinator-mcp/server.ts +270 -1
- package/src/daemon/builtin.ts +46 -0
- package/src/daemon/control-types.ts +65 -0
- package/src/daemon/runtime.ts +51 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
- package/src/edit/modes/replace.ts +1 -1
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +12 -4
- package/src/gjc-runtime/launch-tmux.ts +10 -2
- package/src/gjc-runtime/state-runtime.ts +18 -4
- package/src/gjc-runtime/state-writer.ts +8 -8
- package/src/gjc-runtime/tmux-common.ts +8 -0
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/gjc-runtime/ultragoal-guard.ts +57 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +105 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
- package/src/gjc-runtime/workflow-manifest.ts +11 -1
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/hashline/hash.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +9 -7
- package/src/main.ts +30 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- package/src/modes/components/hook-editor.ts +7 -2
- package/src/modes/components/oauth-selector.ts +19 -0
- package/src/modes/controllers/event-controller.ts +20 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +6 -2
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/event-contract.ts +1 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
- package/src/modes/shared/agent-wire/event-observation.ts +16 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +107 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +700 -0
- package/src/notifications/rate-limit-pool.ts +179 -0
- package/src/notifications/telegram-cli.ts +194 -0
- package/src/notifications/telegram-daemon-cli.ts +74 -0
- package/src/notifications/telegram-daemon-control.ts +370 -0
- package/src/notifications/telegram-daemon.ts +1370 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +80 -0
- package/src/notifications/threaded-render.ts +155 -0
- package/src/notifications/topic-registry.ts +133 -0
- package/src/rlm/index.ts +19 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +113 -3
- package/src/session/auth-storage.ts +3 -0
- package/src/session/session-dump-format.ts +43 -2
- package/src/session/session-manager.ts +39 -5
- package/src/setup/credential-auto-import.ts +258 -0
- package/src/setup/credential-import.ts +17 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
- package/src/setup/host-plugin-setup.ts +142 -0
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/executor.ts +5 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +77 -6
- package/src/tools/image-gen.ts +5 -8
- package/src/tools/index.ts +19 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- package/src/tools/subagent.ts +38 -7
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AuthCredentialIfAbsentReason, AuthCredentialIfAbsentSnapshotResult, AuthStorage } from "@gajae-code/ai";
|
|
2
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
3
|
+
import { type CredentialDiscoveryResult, type CredentialOrigin, type DiscoveryOptions, type ImportableCredential } from "./credential-import";
|
|
4
|
+
export declare const CREDENTIAL_AUTO_IMPORT_ROTATION_WARNING = "Refreshing in gjc may log out the Claude/Codex CLI because OAuth refresh tokens can rotate.";
|
|
5
|
+
export type CredentialAutoImportSourceLabel = "claude-code-file" | "claude-code-keychain" | "codex-file";
|
|
6
|
+
export type CredentialAutoImportTrigger = "startup" | "bare-login" | "setup-cli";
|
|
7
|
+
export declare function getCredentialAutoImportStatePath(agentDir?: string): string;
|
|
8
|
+
export declare function readCredentialImportMarker(agentDir?: string): Promise<string | undefined>;
|
|
9
|
+
export declare function writeCredentialImportMarker(version: string, agentDir?: string): Promise<boolean>;
|
|
10
|
+
export declare enum CredentialAutoImportFailureClass {
|
|
11
|
+
DiscoveryUnavailable = "discovery-unavailable",
|
|
12
|
+
SourceUnreadable = "source-unreadable",
|
|
13
|
+
SourceMalformed = "source-malformed",
|
|
14
|
+
KeychainDenied = "keychain-denied",
|
|
15
|
+
WriteInvalid = "write-invalid",
|
|
16
|
+
WriteConflict = "write-conflict",
|
|
17
|
+
BrokerUnavailable = "broker-unavailable",
|
|
18
|
+
BrokerUnsupported = "broker-unsupported",
|
|
19
|
+
Unknown = "unknown"
|
|
20
|
+
}
|
|
21
|
+
export interface CredentialAutoImportSkipped {
|
|
22
|
+
credential: ImportableCredential;
|
|
23
|
+
reason: AuthCredentialIfAbsentReason;
|
|
24
|
+
entries: AuthCredentialIfAbsentSnapshotResult["entries"];
|
|
25
|
+
}
|
|
26
|
+
export interface CredentialAutoImportFailure {
|
|
27
|
+
credential?: ImportableCredential;
|
|
28
|
+
origin?: CredentialOrigin;
|
|
29
|
+
source?: string;
|
|
30
|
+
failureClass: CredentialAutoImportFailureClass;
|
|
31
|
+
}
|
|
32
|
+
export interface CredentialAutoImportResult {
|
|
33
|
+
imported: ImportableCredential[];
|
|
34
|
+
skipped: CredentialAutoImportSkipped[];
|
|
35
|
+
failures: CredentialAutoImportFailure[];
|
|
36
|
+
discovered: boolean;
|
|
37
|
+
discovery?: CredentialDiscoveryResult;
|
|
38
|
+
globalDiscoveryFailure?: CredentialAutoImportFailure;
|
|
39
|
+
}
|
|
40
|
+
export type CredentialAutoImportAuthStorage = Pick<AuthStorage, "importCredentialIfAbsent">;
|
|
41
|
+
export interface CredentialAutoImportOptions {
|
|
42
|
+
authStorage: CredentialAutoImportAuthStorage;
|
|
43
|
+
discover?: (options?: DiscoveryOptions) => Promise<CredentialDiscoveryResult>;
|
|
44
|
+
discoveryOptions?: DiscoveryOptions;
|
|
45
|
+
trigger: CredentialAutoImportTrigger;
|
|
46
|
+
sourceLabel?: CredentialAutoImportSourceLabel;
|
|
47
|
+
}
|
|
48
|
+
export declare function runExternalCredentialAutoImport({ authStorage, discover, discoveryOptions }: CredentialAutoImportOptions): Promise<CredentialAutoImportResult>;
|
|
49
|
+
export declare function buildCredentialAutoImportNotice(result: Pick<CredentialAutoImportResult, "imported">): string | undefined;
|
|
50
|
+
export declare function formatCredentialAutoImportResult(result: CredentialAutoImportResult): string[];
|
|
51
|
+
export interface CredentialImportMarkerStore {
|
|
52
|
+
read: () => Promise<string | undefined> | string | undefined;
|
|
53
|
+
write: (version: string) => Promise<boolean> | boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface StartupCredentialAutoImportOptions {
|
|
56
|
+
authStorage: CredentialAutoImportOptions["authStorage"];
|
|
57
|
+
modelRegistry: Pick<ModelRegistry, "refresh">;
|
|
58
|
+
discover?: CredentialAutoImportOptions["discover"];
|
|
59
|
+
version?: string;
|
|
60
|
+
agentDir?: string;
|
|
61
|
+
markerStore?: CredentialImportMarkerStore;
|
|
62
|
+
}
|
|
63
|
+
export declare function runStartupCredentialAutoImportIfNeeded({ authStorage: activeAuthStorage, modelRegistry: activeModelRegistry, discover, version, agentDir, markerStore }: StartupCredentialAutoImportOptions): Promise<string | undefined>;
|
|
@@ -3,6 +3,7 @@ import type { AuthCredential } from "@gajae-code/ai";
|
|
|
3
3
|
export type ExternalProvider = "anthropic" | "openai-codex";
|
|
4
4
|
/** Where a discovered credential came from. */
|
|
5
5
|
export type CredentialOrigin = "claude-code-file" | "claude-code-keychain" | "codex-file";
|
|
6
|
+
export declare const AUTO_IMPORT_OAUTH_PROVIDER_ORIGINS: Record<ExternalProvider, ReadonlySet<CredentialOrigin>>;
|
|
6
7
|
/** Human labels for providers, used in redacted summaries. */
|
|
7
8
|
export declare const EXTERNAL_PROVIDER_LABELS: Record<ExternalProvider, string>;
|
|
8
9
|
/** A credential that can be safely imported into gjc's store. */
|
|
@@ -72,6 +73,8 @@ export declare function discoverExternalCredentials(options?: DiscoveryOptions):
|
|
|
72
73
|
export declare function formatCredentialSummary(credential: ImportableCredential): string;
|
|
73
74
|
/** Redacted summary lines for an entire discovery result. Never includes secrets. */
|
|
74
75
|
export declare function formatDiscoverySummary(result: CredentialDiscoveryResult): string[];
|
|
76
|
+
export declare function isAutoImportOAuthCredential(credential: ImportableCredential): boolean;
|
|
77
|
+
export declare function filterAutoImportOAuthCredentials(credentials: readonly ImportableCredential[]): ImportableCredential[];
|
|
75
78
|
/**
|
|
76
79
|
* Persist discovered credentials via `upsert`. Each credential is imported
|
|
77
80
|
* independently; a failure on one is recorded without aborting the rest.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host plugin setup for `gjc setup claude` and `gjc setup codex`.
|
|
3
|
+
*
|
|
4
|
+
* Renders install guidance and a fail-closed coordinator MCP config preview for
|
|
5
|
+
* the canonical generated plugin bundle under `plugins/`. This is intentionally
|
|
6
|
+
* render-only and fail-closed: the workdir allowlist is scoped to the project
|
|
7
|
+
* root and no mutation class is enabled until the user opts in.
|
|
8
|
+
*/
|
|
9
|
+
export type HostPluginKind = "claude" | "codex";
|
|
10
|
+
export interface HostPluginSetupFlags {
|
|
11
|
+
json?: boolean;
|
|
12
|
+
check?: boolean;
|
|
13
|
+
root?: string[];
|
|
14
|
+
repo?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface HostPluginSetupResult {
|
|
17
|
+
ok: true;
|
|
18
|
+
host: HostPluginKind;
|
|
19
|
+
mode: "render";
|
|
20
|
+
gated: boolean;
|
|
21
|
+
pluginPath: string;
|
|
22
|
+
manifestPath: string;
|
|
23
|
+
marketplacePath: string;
|
|
24
|
+
installGuidance: string[];
|
|
25
|
+
coordinatorConfigPreview: {
|
|
26
|
+
command: string;
|
|
27
|
+
args: string[];
|
|
28
|
+
env: Record<string, string>;
|
|
29
|
+
};
|
|
30
|
+
mutationPolicy: string;
|
|
31
|
+
notes: string[];
|
|
32
|
+
check?: {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
checked: string[];
|
|
35
|
+
missing: string[];
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export declare function buildHostPluginSetup(host: HostPluginKind, flags?: HostPluginSetupFlags): HostPluginSetupResult;
|
|
39
|
+
export declare function formatHostPluginSetup(result: HostPluginSetupResult): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-wide registry mapping a session id to its active {@link AskAnswerSource}.
|
|
3
|
+
*
|
|
4
|
+
* Decouples the `ask` tool (which reads the source via `AgentSession`) from the
|
|
5
|
+
* notifications extension (which registers one), without threading a new method
|
|
6
|
+
* through the extension/runner/controller wiring. A session has at most one
|
|
7
|
+
* source; registering returns a disposer.
|
|
8
|
+
*/
|
|
9
|
+
import type { AskAnswerSource } from "./index";
|
|
10
|
+
/** Register `source` for `sessionId`. Returns a disposer that clears it. */
|
|
11
|
+
export declare function registerAskAnswerSource(sessionId: string, source: AskAnswerSource): () => void;
|
|
12
|
+
/** The answer source for `sessionId`, if one is registered. */
|
|
13
|
+
export declare function getAskAnswerSource(sessionId: string): AskAnswerSource | undefined;
|
|
@@ -68,6 +68,17 @@ export type ContextFileEntry = {
|
|
|
68
68
|
};
|
|
69
69
|
export type { DiscoverableMCPTool } from "../runtime-mcp/discoverable-tool-metadata";
|
|
70
70
|
export type { DiscoverableTool, DiscoverableToolSearchIndex, DiscoverableToolSearchResult, DiscoverableToolSource, } from "../tool-discovery/tool-index";
|
|
71
|
+
/**
|
|
72
|
+
* Source of remote answers for interactive asks (e.g. a Telegram reply routed
|
|
73
|
+
* through the notifications SDK). Lets a pending ask resolve without RPC mode.
|
|
74
|
+
*/
|
|
75
|
+
export interface AskAnswerSource {
|
|
76
|
+
/**
|
|
77
|
+
* Race a remote answer against the local UI for one question. Resolves with the
|
|
78
|
+
* chosen option label or free-text answer, or `undefined` to defer to local UI.
|
|
79
|
+
*/
|
|
80
|
+
awaitAnswer(question: string, options: string[], signal?: AbortSignal): Promise<string | undefined>;
|
|
81
|
+
}
|
|
71
82
|
/** Session context for tool factories */
|
|
72
83
|
export interface ToolSession {
|
|
73
84
|
/** Current working directory */
|
|
@@ -167,6 +178,13 @@ export interface ToolSession {
|
|
|
167
178
|
getGoalModeState?: () => GoalModeState | undefined;
|
|
168
179
|
/** Unattended workflow-gate emitter (present only when unattended mode is negotiated). */
|
|
169
180
|
getWorkflowGateEmitter?: () => WorkflowGateEmitter | undefined;
|
|
181
|
+
/**
|
|
182
|
+
* Optional remote answer source for interactive asks. When present, the ask
|
|
183
|
+
* tool races the local UI selection against a remote answer (e.g. a Telegram
|
|
184
|
+
* reply via the notifications SDK) so asks can be answered without RPC mode.
|
|
185
|
+
* No-op when undefined: the ask path behaves exactly as before.
|
|
186
|
+
*/
|
|
187
|
+
getAskAnswerSource?: () => AskAnswerSource | undefined;
|
|
170
188
|
/** Optional per-session restriction for goal tool operations. */
|
|
171
189
|
goalToolAllowedOps?: readonly ("create" | "get" | "complete" | "resume" | "drop" | "pause")[];
|
|
172
190
|
/** Goal runtime for the active agent session. */
|
|
@@ -42,6 +42,9 @@ export interface SubagentSnapshot {
|
|
|
42
42
|
outputRef?: string;
|
|
43
43
|
truncated?: boolean;
|
|
44
44
|
guidance?: string;
|
|
45
|
+
steerMessage?: string;
|
|
46
|
+
steerState?: "queued" | "resume_queued" | "resume_started";
|
|
47
|
+
steerPauseRequested?: boolean;
|
|
45
48
|
/** Live streaming progress for the awaited subagent (await panel only; UI detail). */
|
|
46
49
|
progress?: AgentProgress;
|
|
47
50
|
/** True when a live in-session progress producer exists for this subagent. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/coding-agent",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.1",
|
|
5
5
|
"description": "Gajae Code CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
52
52
|
"@babel/parser": "^7.29.3",
|
|
53
53
|
"@mozilla/readability": "^0.6.0",
|
|
54
|
-
"@gajae-code/stats": "0.
|
|
55
|
-
"@gajae-code/agent-core": "0.
|
|
56
|
-
"@gajae-code/ai": "0.
|
|
57
|
-
"@gajae-code/natives": "0.
|
|
58
|
-
"@gajae-code/tui": "0.
|
|
59
|
-
"@gajae-code/utils": "0.
|
|
54
|
+
"@gajae-code/stats": "0.7.1",
|
|
55
|
+
"@gajae-code/agent-core": "0.7.1",
|
|
56
|
+
"@gajae-code/ai": "0.7.1",
|
|
57
|
+
"@gajae-code/natives": "0.7.1",
|
|
58
|
+
"@gajae-code/tui": "0.7.1",
|
|
59
|
+
"@gajae-code/utils": "0.7.1",
|
|
60
60
|
"@puppeteer/browsers": "^2.13.0",
|
|
61
61
|
"@types/turndown": "5.0.6",
|
|
62
62
|
"@xterm/headless": "^6.0.0",
|
package/scripts/build-binary.ts
CHANGED
|
@@ -66,6 +66,9 @@ async function main(): Promise<void> {
|
|
|
66
66
|
"../stats/src/sync-worker.ts",
|
|
67
67
|
"./src/tools/browser/tab-worker-entry.ts",
|
|
68
68
|
"./src/eval/js/worker-entry.ts",
|
|
69
|
+
// Hidden notify daemon CLI (loaded via dynamic import from notify-cli); list it
|
|
70
|
+
// explicitly so Bun standalone keeps `gjc notify daemon-internal` usable.
|
|
71
|
+
"./src/notifications/telegram-daemon-cli.ts",
|
|
69
72
|
"--outfile",
|
|
70
73
|
"dist/gjc",
|
|
71
74
|
],
|
package/src/async/job-manager.ts
CHANGED
|
@@ -84,7 +84,11 @@ export interface SubagentLiveHandle {
|
|
|
84
84
|
/** Request a cooperative safe-boundary pause (never aborts the in-flight tool). */
|
|
85
85
|
requestPause(): void;
|
|
86
86
|
/** Inject a steering message into the live session. */
|
|
87
|
-
injectMessage(
|
|
87
|
+
injectMessage(
|
|
88
|
+
content: string,
|
|
89
|
+
deliverAs: "steer" | "followUp" | "nextTurn",
|
|
90
|
+
opts?: { fromAgentId?: string },
|
|
91
|
+
): Promise<void>;
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
/**
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gjc daemon` command handler.
|
|
3
|
+
*
|
|
4
|
+
* Generic over the static built-in daemon controller map: lists/inspects
|
|
5
|
+
* daemons and drives cooperative stop/reload. Telegram is the only kind today.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Settings } from "../config/settings";
|
|
9
|
+
import { selectDaemonControllers } from "../daemon/builtin";
|
|
10
|
+
import type {
|
|
11
|
+
BuiltInDaemonController,
|
|
12
|
+
DaemonKind,
|
|
13
|
+
DaemonOperationOptions,
|
|
14
|
+
DaemonOperationResult,
|
|
15
|
+
DaemonStatus,
|
|
16
|
+
} from "../daemon/control-types";
|
|
17
|
+
|
|
18
|
+
export type DaemonCliAction = "list" | "status" | "stop" | "reload";
|
|
19
|
+
|
|
20
|
+
export interface DaemonCommandArgs {
|
|
21
|
+
action: DaemonCliAction;
|
|
22
|
+
kinds: DaemonKind[];
|
|
23
|
+
all: boolean;
|
|
24
|
+
json: boolean;
|
|
25
|
+
force: boolean;
|
|
26
|
+
gracefulTimeoutMs?: number;
|
|
27
|
+
killTimeoutMs?: number;
|
|
28
|
+
spawnIfStopped?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DaemonCommandDeps {
|
|
32
|
+
settings?: Settings;
|
|
33
|
+
controllers?: BuiltInDaemonController[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const KNOWN_ACTIONS: DaemonCliAction[] = ["list", "status", "stop", "reload"];
|
|
37
|
+
const KNOWN_KINDS: DaemonKind[] = ["telegram"];
|
|
38
|
+
|
|
39
|
+
export function parseDaemonArgs(argv: string[]): DaemonCommandArgs | undefined {
|
|
40
|
+
if (argv.length === 0 || argv[0] !== "daemon") return undefined;
|
|
41
|
+
const rest = argv.slice(1);
|
|
42
|
+
const action = (KNOWN_ACTIONS as string[]).includes(rest[0] ?? "") ? (rest[0] as DaemonCliAction) : "status";
|
|
43
|
+
const positional = (KNOWN_ACTIONS as string[]).includes(rest[0] ?? "") ? rest.slice(1) : rest;
|
|
44
|
+
const kinds: DaemonKind[] = [];
|
|
45
|
+
let all = false;
|
|
46
|
+
let json = false;
|
|
47
|
+
let force = false;
|
|
48
|
+
let gracefulTimeoutMs: number | undefined;
|
|
49
|
+
let killTimeoutMs: number | undefined;
|
|
50
|
+
let spawnIfStopped: boolean | undefined;
|
|
51
|
+
for (let i = 0; i < positional.length; i++) {
|
|
52
|
+
const arg = positional[i];
|
|
53
|
+
if (arg === "--all") all = true;
|
|
54
|
+
else if (arg === "--json") json = true;
|
|
55
|
+
else if (arg === "--force") force = true;
|
|
56
|
+
else if (arg === "--spawn-if-stopped") spawnIfStopped = true;
|
|
57
|
+
else if (arg === "--graceful-timeout-ms") gracefulTimeoutMs = Number.parseInt(positional[++i], 10);
|
|
58
|
+
else if (arg === "--kill-timeout-ms") killTimeoutMs = Number.parseInt(positional[++i], 10);
|
|
59
|
+
else if (!arg.startsWith("--") && (KNOWN_KINDS as string[]).includes(arg)) kinds.push(arg as DaemonKind);
|
|
60
|
+
}
|
|
61
|
+
return { action, kinds, all, json, force, gracefulTimeoutMs, killTimeoutMs, spawnIfStopped };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function formatStatus(status: DaemonStatus): string {
|
|
65
|
+
const parts = [
|
|
66
|
+
`${status.kind}: ${status.health}`,
|
|
67
|
+
status.configured ? undefined : "(not configured)",
|
|
68
|
+
status.pid !== undefined ? `pid=${status.pid}` : undefined,
|
|
69
|
+
status.ownerId ? `owner=${status.ownerId}` : undefined,
|
|
70
|
+
status.rootCount !== undefined ? `roots=${status.rootCount}` : undefined,
|
|
71
|
+
`mode=${status.runtime.mode}`,
|
|
72
|
+
].filter(Boolean);
|
|
73
|
+
let line = parts.join(" ");
|
|
74
|
+
if (status.runtime.warning) line += `\n warning: ${status.runtime.warning}`;
|
|
75
|
+
return line;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatResult(result: DaemonOperationResult): string {
|
|
79
|
+
const head = `${result.kind} ${result.action}: ${result.ok ? "ok" : "failed"} — ${result.message}`;
|
|
80
|
+
const warnings = result.warnings.map(w => `\n warning: ${w}`).join("");
|
|
81
|
+
return head + warnings;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function runDaemonCommand(cmd: DaemonCommandArgs, deps: DaemonCommandDeps = {}): Promise<void> {
|
|
85
|
+
const unknownKinds = cmd.kinds.filter(kind => !(KNOWN_KINDS as string[]).includes(kind));
|
|
86
|
+
if (unknownKinds.length > 0) {
|
|
87
|
+
process.stderr.write(
|
|
88
|
+
`Unknown daemon kind(s): ${unknownKinds.join(", ")}. Known kinds: ${KNOWN_KINDS.join(", ")}.\n`,
|
|
89
|
+
);
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const settings = deps.settings ?? (await Settings.init());
|
|
94
|
+
const controllers = deps.controllers ?? selectDaemonControllers(settings, cmd.kinds, cmd.all);
|
|
95
|
+
|
|
96
|
+
if (cmd.action === "list" || cmd.action === "status") {
|
|
97
|
+
const statuses = await Promise.all(controllers.map(c => c.status()));
|
|
98
|
+
if (cmd.json) {
|
|
99
|
+
process.stdout.write(`${JSON.stringify(statuses, null, 2)}\n`);
|
|
100
|
+
} else {
|
|
101
|
+
process.stdout.write(`${statuses.map(formatStatus).join("\n")}\n`);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const opts: DaemonOperationOptions = {
|
|
107
|
+
gracefulTimeoutMs: cmd.gracefulTimeoutMs,
|
|
108
|
+
killTimeoutMs: cmd.killTimeoutMs,
|
|
109
|
+
force: cmd.force,
|
|
110
|
+
spawnIfStopped: cmd.spawnIfStopped,
|
|
111
|
+
};
|
|
112
|
+
const results: DaemonOperationResult[] = [];
|
|
113
|
+
for (const controller of controllers) {
|
|
114
|
+
results.push(cmd.action === "reload" ? await controller.reload(opts) : await controller.stop(opts));
|
|
115
|
+
}
|
|
116
|
+
if (cmd.json) {
|
|
117
|
+
process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
|
|
118
|
+
} else {
|
|
119
|
+
process.stdout.write(`${results.map(formatResult).join("\n")}\n`);
|
|
120
|
+
}
|
|
121
|
+
if (results.some(r => !r.ok)) process.exitCode = 1;
|
|
122
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notify CLI command handlers.
|
|
3
|
+
*
|
|
4
|
+
* Handles `gjc notify` setup/status and the hidden daemon entrypoint.
|
|
5
|
+
*/
|
|
6
|
+
import { createInterface } from "node:readline/promises";
|
|
7
|
+
import { APP_NAME } from "@gajae-code/utils";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { Settings } from "../config/settings";
|
|
10
|
+
import { getNotificationConfig, maskToken } from "../notifications/config";
|
|
11
|
+
|
|
12
|
+
export type NotifyAction = "setup" | "status" | "daemon-internal";
|
|
13
|
+
|
|
14
|
+
export interface NotifyCommandArgs {
|
|
15
|
+
action: NotifyAction;
|
|
16
|
+
smoke?: boolean;
|
|
17
|
+
rawArgs: string[];
|
|
18
|
+
token?: string;
|
|
19
|
+
chatId?: string;
|
|
20
|
+
redact?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface NotifyCommandDeps {
|
|
24
|
+
fetchImpl?: typeof fetch;
|
|
25
|
+
apiBase?: string;
|
|
26
|
+
settings?: Settings;
|
|
27
|
+
setupToken?: string;
|
|
28
|
+
pollTimeoutMs?: number;
|
|
29
|
+
pollIntervalMs?: number;
|
|
30
|
+
setupChatId?: string;
|
|
31
|
+
setupRedact?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface TelegramApiResponse<T> {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
result?: T;
|
|
37
|
+
description?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface TelegramUpdate {
|
|
41
|
+
update_id: number;
|
|
42
|
+
message?: {
|
|
43
|
+
chat?: {
|
|
44
|
+
id?: number | string;
|
|
45
|
+
type?: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const DEFAULT_API_BASE = "https://api.telegram.org";
|
|
51
|
+
const DEFAULT_POLL_TIMEOUT_MS = 60_000;
|
|
52
|
+
const DEFAULT_POLL_INTERVAL_MS = 1_000;
|
|
53
|
+
|
|
54
|
+
export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
|
|
55
|
+
if (args.length === 0 || args[0] !== "notify") {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const action = args[1];
|
|
60
|
+
if (action === "setup" || action === "status") {
|
|
61
|
+
const rest = args.slice(2);
|
|
62
|
+
const flag = (name: string): string | undefined => {
|
|
63
|
+
const i = rest.indexOf(name);
|
|
64
|
+
return i >= 0 ? rest[i + 1] : undefined;
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
action,
|
|
68
|
+
rawArgs: rest,
|
|
69
|
+
token: flag("--token"),
|
|
70
|
+
chatId: flag("--chat-id"),
|
|
71
|
+
redact: rest.includes("--redact"),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (action === "daemon-internal") {
|
|
75
|
+
return {
|
|
76
|
+
action,
|
|
77
|
+
smoke: args.slice(2).includes("--smoke"),
|
|
78
|
+
rawArgs: args.slice(2),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { action: "status", rawArgs: args.slice(1) };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function runNotifyCommand(cmd: NotifyCommandArgs, deps: NotifyCommandDeps = {}): Promise<void> {
|
|
86
|
+
switch (cmd.action) {
|
|
87
|
+
case "setup":
|
|
88
|
+
await runSetup({
|
|
89
|
+
...deps,
|
|
90
|
+
setupToken: deps.setupToken ?? cmd.token,
|
|
91
|
+
setupChatId: deps.setupChatId ?? cmd.chatId,
|
|
92
|
+
setupRedact: deps.setupRedact ?? cmd.redact,
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
case "status":
|
|
96
|
+
await runStatus(deps);
|
|
97
|
+
return;
|
|
98
|
+
case "daemon-internal": {
|
|
99
|
+
const m = await import("../notifications/telegram-daemon-cli");
|
|
100
|
+
if (cmd.smoke) {
|
|
101
|
+
await m.runDaemonSmoke();
|
|
102
|
+
} else {
|
|
103
|
+
await m.runDaemonInternal(cmd.rawArgs);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function getSettings(deps: NotifyCommandDeps): Promise<Settings> {
|
|
111
|
+
return deps.settings ?? (await Settings.init());
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function runSetup(deps: NotifyCommandDeps): Promise<void> {
|
|
115
|
+
const settings = await getSettings(deps);
|
|
116
|
+
const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
|
|
117
|
+
const apiBase = deps.apiBase ?? DEFAULT_API_BASE;
|
|
118
|
+
const token = deps.setupToken ?? (await promptForToken());
|
|
119
|
+
if (!token.trim()) {
|
|
120
|
+
throw new Error("Telegram bot token is required.");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await callTelegram(fetchImpl, apiBase, token, "getMe", {});
|
|
124
|
+
process.stdout.write(
|
|
125
|
+
"Token validated. Message your bot now from the private Telegram chat to pair notifications.\n",
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
let chatId: string;
|
|
129
|
+
if (deps.setupChatId?.trim()) {
|
|
130
|
+
chatId = deps.setupChatId.trim();
|
|
131
|
+
process.stdout.write(`Using provided chat id ${chatId} (non-interactive).\n`);
|
|
132
|
+
} else {
|
|
133
|
+
const stale = await getUpdates(fetchImpl, apiBase, token, { timeout: 0, allowed_updates: ["message"] });
|
|
134
|
+
const offset = nextOffset(stale);
|
|
135
|
+
chatId = await waitForPrivateChat(fetchImpl, apiBase, token, {
|
|
136
|
+
offset,
|
|
137
|
+
pollTimeoutMs: deps.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
|
|
138
|
+
pollIntervalMs: deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
settings.set("notifications.telegram.botToken", token);
|
|
143
|
+
settings.set("notifications.telegram.chatId", chatId);
|
|
144
|
+
settings.set("notifications.enabled", true);
|
|
145
|
+
if (deps.setupRedact) settings.set("notifications.redact", true);
|
|
146
|
+
await settings.flush();
|
|
147
|
+
|
|
148
|
+
process.stdout.write(`Notifications enabled. botToken=${maskToken(token)} chatId=${chatId}\n`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function promptForToken(): Promise<string> {
|
|
152
|
+
if (!process.stdin.isTTY) {
|
|
153
|
+
throw new Error("notify setup requires an interactive TTY unless setupToken is injected.");
|
|
154
|
+
}
|
|
155
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
156
|
+
try {
|
|
157
|
+
return (await rl.question("Telegram BotFather token: ")).trim();
|
|
158
|
+
} finally {
|
|
159
|
+
rl.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function runStatus(deps: NotifyCommandDeps): Promise<void> {
|
|
164
|
+
const settings = await getSettings(deps);
|
|
165
|
+
const cfg = getNotificationConfig(settings);
|
|
166
|
+
process.stdout.write(
|
|
167
|
+
`${chalk.bold("Notifications")}\n` +
|
|
168
|
+
` enabled: ${cfg.enabled}\n` +
|
|
169
|
+
` botToken: ${maskToken(cfg.botToken)}\n` +
|
|
170
|
+
` chatId: ${cfg.chatId ?? "(unset)"}\n` +
|
|
171
|
+
` redact: ${cfg.redact}\n`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function waitForPrivateChat(
|
|
176
|
+
fetchImpl: typeof fetch,
|
|
177
|
+
apiBase: string,
|
|
178
|
+
token: string,
|
|
179
|
+
opts: { offset: number | undefined; pollTimeoutMs: number; pollIntervalMs: number },
|
|
180
|
+
): Promise<string> {
|
|
181
|
+
const deadline = Date.now() + opts.pollTimeoutMs;
|
|
182
|
+
let offset = opts.offset;
|
|
183
|
+
let sawRejectedChatType: string | undefined;
|
|
184
|
+
|
|
185
|
+
while (Date.now() <= deadline) {
|
|
186
|
+
const updates = await getUpdates(fetchImpl, apiBase, token, { offset, timeout: 0, allowed_updates: ["message"] });
|
|
187
|
+
offset = nextOffset(updates, offset);
|
|
188
|
+
for (const update of updates) {
|
|
189
|
+
const chat = update.message?.chat;
|
|
190
|
+
if (!chat) continue;
|
|
191
|
+
if (chat.type === "private" && chat.id !== undefined) {
|
|
192
|
+
return String(chat.id);
|
|
193
|
+
}
|
|
194
|
+
if (chat.type === "group" || chat.type === "supergroup" || chat.type === "channel") {
|
|
195
|
+
sawRejectedChatType = chat.type;
|
|
196
|
+
process.stderr.write(
|
|
197
|
+
`Rejected ${chat.type} chat. Pairing requires a private Telegram chat with the bot.\n`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (opts.pollIntervalMs > 0) {
|
|
202
|
+
await new Promise(resolve =>
|
|
203
|
+
setTimeout(resolve, Math.min(opts.pollIntervalMs, Math.max(0, deadline - Date.now()))),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (sawRejectedChatType) {
|
|
209
|
+
throw new Error(`Pairing rejected ${sawRejectedChatType} chat; message the bot from a private chat.`);
|
|
210
|
+
}
|
|
211
|
+
throw new Error("Timed out waiting for a private Telegram message to pair notifications.");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function nextOffset(updates: TelegramUpdate[], fallback?: number): number | undefined {
|
|
215
|
+
let max = fallback === undefined ? undefined : fallback - 1;
|
|
216
|
+
for (const update of updates) {
|
|
217
|
+
if (typeof update.update_id === "number" && (max === undefined || update.update_id > max)) {
|
|
218
|
+
max = update.update_id;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return max === undefined ? fallback : max + 1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function getUpdates(
|
|
225
|
+
fetchImpl: typeof fetch,
|
|
226
|
+
apiBase: string,
|
|
227
|
+
token: string,
|
|
228
|
+
params: Record<string, unknown>,
|
|
229
|
+
): Promise<TelegramUpdate[]> {
|
|
230
|
+
return await callTelegram<TelegramUpdate[]>(fetchImpl, apiBase, token, "getUpdates", params);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function callTelegram<T>(
|
|
234
|
+
fetchImpl: typeof fetch,
|
|
235
|
+
apiBase: string,
|
|
236
|
+
token: string,
|
|
237
|
+
method: string,
|
|
238
|
+
body: Record<string, unknown>,
|
|
239
|
+
): Promise<T> {
|
|
240
|
+
const response = await fetchImpl(`${apiBase.replace(/\/$/, "")}/bot${token}/${method}`, {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers: { "content-type": "application/json" },
|
|
243
|
+
body: JSON.stringify(body),
|
|
244
|
+
});
|
|
245
|
+
let payload: TelegramApiResponse<T>;
|
|
246
|
+
try {
|
|
247
|
+
payload = (await response.json()) as TelegramApiResponse<T>;
|
|
248
|
+
} catch {
|
|
249
|
+
throw new Error(`Telegram ${method} returned invalid JSON.`);
|
|
250
|
+
}
|
|
251
|
+
if (!response.ok || !payload.ok) {
|
|
252
|
+
throw new Error(`Telegram ${method} failed: ${payload.description ?? response.statusText}`);
|
|
253
|
+
}
|
|
254
|
+
return payload.result as T;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function printNotifyHelp(): void {
|
|
258
|
+
process.stdout.write(`${chalk.bold(`${APP_NAME} notify`)} - Configure Telegram notifications
|
|
259
|
+
|
|
260
|
+
${chalk.bold("Usage:")}
|
|
261
|
+
${APP_NAME} notify setup
|
|
262
|
+
${APP_NAME} notify setup --token <botToken> --chat-id <chatId> [--redact]
|
|
263
|
+
${APP_NAME} notify status
|
|
264
|
+
|
|
265
|
+
${chalk.bold("Subcommands:")}
|
|
266
|
+
setup Pair a Telegram bot token with a private chat
|
|
267
|
+
status Show notification configuration without secrets
|
|
268
|
+
|
|
269
|
+
${chalk.bold("Examples:")}
|
|
270
|
+
${APP_NAME} notify setup
|
|
271
|
+
${APP_NAME} notify setup --token <botToken> --chat-id <chatId> [--redact]
|
|
272
|
+
${APP_NAME} notify status
|
|
273
|
+
`);
|
|
274
|
+
}
|