@gajae-code/coding-agent 0.9.0 → 0.9.2

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.
Files changed (98) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/types/cli/args.d.ts +1 -0
  3. package/dist/types/cli/notify-cli.d.ts +5 -0
  4. package/dist/types/cli.d.ts +3 -0
  5. package/dist/types/commands/launch.d.ts +3 -0
  6. package/dist/types/config/model-profile-activation.d.ts +7 -7
  7. package/dist/types/config/model-registry.d.ts +7 -3
  8. package/dist/types/config/model-resolver.d.ts +2 -2
  9. package/dist/types/config/settings-schema.d.ts +15 -1
  10. package/dist/types/config/settings.d.ts +7 -0
  11. package/dist/types/extensibility/runtime-skill-discovery.d.ts +20 -0
  12. package/dist/types/gjc-runtime/team-runtime.d.ts +9 -0
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  14. package/dist/types/modes/components/settings-selector.d.ts +2 -2
  15. package/dist/types/modes/components/thinking-selector.d.ts +7 -2
  16. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  17. package/dist/types/modes/interactive-mode.d.ts +2 -0
  18. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
  19. package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
  20. package/dist/types/modes/types.d.ts +3 -0
  21. package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
  22. package/dist/types/notifications/config-commands.d.ts +38 -0
  23. package/dist/types/notifications/config.d.ts +16 -0
  24. package/dist/types/notifications/index.d.ts +14 -1
  25. package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
  26. package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
  27. package/dist/types/notifications/reply-sent-store.d.ts +53 -0
  28. package/dist/types/notifications/rich-draft.d.ts +68 -0
  29. package/dist/types/notifications/rich-render.d.ts +90 -0
  30. package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
  31. package/dist/types/notifications/telegram-daemon.d.ts +22 -0
  32. package/dist/types/notifications/telegram-reference.d.ts +7 -0
  33. package/dist/types/notifications/threaded-render.d.ts +10 -0
  34. package/dist/types/notifications/topic-registry.d.ts +4 -5
  35. package/dist/types/runtime-credential-selector.d.ts +7 -0
  36. package/dist/types/sdk.d.ts +7 -1
  37. package/dist/types/session/agent-session.d.ts +6 -3
  38. package/dist/types/tools/index.d.ts +1 -0
  39. package/dist/types/tools/skill-discovery.d.ts +40 -0
  40. package/dist/types/utils/pasted-image-path.d.ts +30 -0
  41. package/package.json +8 -11
  42. package/src/cli/args.ts +8 -0
  43. package/src/cli/notify-cli.ts +38 -10
  44. package/src/cli.ts +5 -0
  45. package/src/commands/launch.ts +5 -0
  46. package/src/commands/notify.ts +2 -2
  47. package/src/config/keybindings.ts +1 -1
  48. package/src/config/model-profile-activation.ts +23 -7
  49. package/src/config/model-registry.ts +21 -4
  50. package/src/config/model-resolver.ts +5 -16
  51. package/src/config/settings-schema.ts +11 -2
  52. package/src/config/settings.ts +24 -1
  53. package/src/extensibility/runtime-skill-discovery.ts +229 -0
  54. package/src/gjc-runtime/launch-tmux.ts +78 -12
  55. package/src/gjc-runtime/psmux-detect.ts +17 -2
  56. package/src/gjc-runtime/team-runtime.ts +46 -7
  57. package/src/gjc-runtime/tmux-common.ts +1 -0
  58. package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
  59. package/src/internal-urls/docs-index.generated.ts +6 -6
  60. package/src/lsp/client.ts +1 -0
  61. package/src/main.ts +26 -3
  62. package/src/modes/components/footer.ts +7 -2
  63. package/src/modes/components/settings-selector.ts +3 -3
  64. package/src/modes/components/thinking-selector.ts +83 -20
  65. package/src/modes/controllers/event-controller.ts +11 -2
  66. package/src/modes/controllers/extension-ui-controller.ts +8 -1
  67. package/src/modes/controllers/input-controller.ts +62 -26
  68. package/src/modes/controllers/selector-controller.ts +43 -0
  69. package/src/modes/interactive-mode.ts +5 -0
  70. package/src/modes/rpc/rpc-mode.ts +3 -9
  71. package/src/modes/rpc/rpc-socket-security.ts +13 -1
  72. package/src/modes/types.ts +3 -0
  73. package/src/modes/utils/injected-user-submission.ts +94 -0
  74. package/src/modes/utils/ui-helpers.ts +48 -15
  75. package/src/notifications/config-commands.ts +90 -0
  76. package/src/notifications/config.ts +27 -0
  77. package/src/notifications/index.ts +184 -14
  78. package/src/notifications/lifecycle-commands.ts +37 -16
  79. package/src/notifications/lifecycle-control-runtime.ts +8 -3
  80. package/src/notifications/reply-sent-store.ts +134 -0
  81. package/src/notifications/rich-draft.ts +107 -0
  82. package/src/notifications/rich-render.ts +143 -0
  83. package/src/notifications/telegram-daemon-cli.ts +60 -5
  84. package/src/notifications/telegram-daemon.ts +431 -91
  85. package/src/notifications/telegram-reference.ts +17 -0
  86. package/src/notifications/threaded-render.ts +42 -1
  87. package/src/notifications/topic-registry.ts +9 -8
  88. package/src/prompts/tools/search-tool-bm25.md +5 -0
  89. package/src/prompts/tools/skill-discovery.md +13 -0
  90. package/src/runtime-credential-selector.ts +35 -0
  91. package/src/sdk.ts +104 -13
  92. package/src/session/agent-session.ts +48 -22
  93. package/src/slash-commands/builtin-registry.ts +78 -1
  94. package/src/system-prompt.ts +4 -6
  95. package/src/tools/index.ts +4 -0
  96. package/src/tools/skill-discovery.ts +73 -0
  97. package/src/tools/skill.ts +15 -2
  98. package/src/utils/pasted-image-path.ts +170 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Index of rich messages the daemon sent, mapping `${chat_id}:${message_id}` to
3
+ * the original markdown. Telegram does not echo a `sendRichMessage` message's
4
+ * text in a reply's `reply_to_message`, so a user replying to a rich final
5
+ * answer would otherwise strand the agent without the quoted context. The daemon
6
+ * records the markdown on every promoted send and looks it up on inbound replies
7
+ * to restore that context.
8
+ *
9
+ * Fail-closed and off-path-neutral: nothing is recorded unless a send was
10
+ * actually promoted to rich, `record` is a no-op on any error (the in-memory map
11
+ * is only mutated after the atomic persist succeeds), and `lookup` returns
12
+ * undefined on any miss or error, so a bad index write/read never kills the
13
+ * daemon.
14
+ */
15
+ import * as fs from "node:fs";
16
+ /**
17
+ * Minimal async filesystem surface the store needs. Structurally a subset of the
18
+ * daemon's `TelegramDaemonFs`, so the daemon can pass its own `fs` straight
19
+ * through and tests can inject a fake.
20
+ */
21
+ export interface ReplySentStoreFs {
22
+ mkdir(path: string, opts?: fs.MakeDirectoryOptions): Promise<unknown>;
23
+ readFile(path: string, encoding: BufferEncoding): Promise<string>;
24
+ writeFile(path: string, data: string, opts?: fs.WriteFileOptions): Promise<void>;
25
+ rename(oldPath: string, newPath: string): Promise<void>;
26
+ chmod(path: string, mode: number): Promise<void>;
27
+ }
28
+ export declare class ReplySentStore {
29
+ #private;
30
+ constructor(input: {
31
+ agentDir: string;
32
+ fs?: ReplySentStoreFs;
33
+ now?: () => number;
34
+ });
35
+ /** Restore the persisted index into memory. No-op on a missing or corrupt file. */
36
+ load(): Promise<void>;
37
+ /**
38
+ * Record the original markdown of a rich message the daemon just sent. The
39
+ * text is capped at {@link MAX_TEXT_LENGTH}; the index is capped at
40
+ * {@link MAX_ENTRIES} (oldest-by-timestamp evicted). No-op on any failure: the
41
+ * in-memory map is only replaced after the atomic persist succeeds.
42
+ */
43
+ record(input: {
44
+ chatId: string | number;
45
+ messageId: number;
46
+ text: string;
47
+ }): Promise<void>;
48
+ /** Look up the original markdown for a message the daemon sent. undefined on miss or failure. */
49
+ lookup(input: {
50
+ chatId: string | number;
51
+ messageId: number;
52
+ }): string | undefined;
53
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Opt-in rich-draft streaming for a turn's in-progress preview.
3
+ *
4
+ * When the operator opts in (`richDraft.enabled`) the daemon streams LIVE turn
5
+ * markdown to the Bot API `sendRichMessageDraft` method as a debounced preview.
6
+ * Every failure is a harmless no-op (warn once, keep the daemon alive), and when
7
+ * off no draft call is ever made — so the off-state request bodies stay
8
+ * byte-identical.
9
+ */
10
+ import type { BotApi } from "./telegram-daemon";
11
+ import type { ThreadedSend } from "./threaded-render";
12
+ /** Minimum gap between two draft sends for one session (debounce floor). */
13
+ export declare const DRAFT_DEBOUNCE_MS = 1500;
14
+ /**
15
+ * Wrap raw markdown + a monotonic draft id in the `sendRichMessageDraft` request
16
+ * payload shape: mirrors `buildRichMessage`'s proven `rich_message.markdown`
17
+ * content wrapper, with a top-level `draft_id` selecting the draft revision to
18
+ * update (a routing param, like `message_thread_id`).
19
+ */
20
+ export declare function buildRichDraft(draftId: number, raw: string): {
21
+ draft_id: number;
22
+ rich_message: {
23
+ markdown: string;
24
+ };
25
+ };
26
+ /**
27
+ * Whether a granted send should stream a rich draft. Fail-closed: every clause
28
+ * must hold, otherwise no draft is sent. Mirrors `shouldPromoteRich` but targets
29
+ * the LIVE lane and the live-only `richDraftMarkdown` marker (set by
30
+ * `renderThreadedFrame` for non-finalized turn frames).
31
+ */
32
+ export declare function shouldStreamDraft(input: {
33
+ enabled?: boolean;
34
+ send: ThreadedSend;
35
+ }): boolean;
36
+ /**
37
+ * Per-session debounce + monotonic draft-id state for draft streaming. Skips a
38
+ * draft when less than `debounceMs` has elapsed since the session's last SENT
39
+ * draft (the rate-limit pool already coalesces live frames to the latest, so a
40
+ * skipped frame is naturally superseded by the next one). `reset` clears a
41
+ * session's window when its turn finalizes so the next turn starts fresh.
42
+ */
43
+ export declare class DraftStreamState {
44
+ #private;
45
+ constructor(debounceMs?: number);
46
+ /**
47
+ * If enough time has elapsed since the session's last draft, record `now` as
48
+ * the new last-sent time and return the next monotonic draft id; otherwise
49
+ * return `undefined` (debounced — the caller skips this frame).
50
+ */
51
+ tryClaim(sessionId: string, now: number): number | undefined;
52
+ /** Clear a session's debounce window (called when its turn finalizes). The
53
+ * global draft id keeps incrementing so ids are never reused across turns. */
54
+ reset(sessionId: string): void;
55
+ }
56
+ /**
57
+ * Best-effort delivery of a rich draft. Never throws and has no HTML fallback: a
58
+ * draft is a purely additive preview, so on any failure (a thrown transport
59
+ * error or an `{ ok: false }` JSON response — the transport returns `res.json()`
60
+ * for JSON methods, so `ok:false` does not throw) it warns exactly once and
61
+ * returns; the unchanged live HTML send still carries the content.
62
+ */
63
+ export declare function deliverDraft(botApi: BotApi, base: {
64
+ chat_id: string | number;
65
+ message_thread_id?: number;
66
+ }, draftId: number, raw: string, log?: {
67
+ warn(msg: string): void;
68
+ }): Promise<void>;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Rich-message promotion for stable non-editable Telegram text sends.
3
+ *
4
+ * When enabled, the daemon promotes eligible finalized `sendMessage` payloads
5
+ * carrying raw markdown to the Bot API `sendRichMessage` method. On any miss or
6
+ * failure the daemon keeps the unchanged HTML `sendMessage` path, so the
7
+ * off-state request bodies are byte-identical.
8
+ */
9
+ import type { BotApi } from "./telegram-daemon";
10
+ import type { ThreadedSend } from "./threaded-render";
11
+ /**
12
+ * Telegram's hard per-message character ceiling (4096). Surfaced here purely as
13
+ * documentation and a marker for a future native rich-message splitter — it is
14
+ * intentionally NON-BEHAVIORAL and MUST stay that way: nothing in the rich path
15
+ * branches on this value.
16
+ *
17
+ * Overflow is already safe without it. The production final-answer text is capped
18
+ * at 3500 chars upstream (`summaryFromMessage(..., 3500)`), so a promoted
19
+ * `sendRichMessage` never approaches this ceiling; and if the Bot API ever rejects
20
+ * an oversized rich payload it returns `{ ok: false }`, which
21
+ * `deliverRichWithFallback` (below) turns into the chunked HTML `splitTelegramHtml`
22
+ * fallback (each chunk ≤ TELEGRAM_MESSAGE_LIMIT). This constant only marks where a
23
+ * future rich splitter would read its ceiling; wiring it into a branch would change
24
+ * byte-for-byte behavior and is out of scope.
25
+ */
26
+ export declare const RICH_MESSAGE_LIMIT = 4096;
27
+ /** Wrap raw markdown in the `sendRichMessage` request payload shape. */
28
+ export declare function buildRichMessage(raw: string, extras?: {
29
+ reply_markup?: unknown;
30
+ }): {
31
+ rich_message: {
32
+ markdown: string;
33
+ };
34
+ reply_markup?: unknown;
35
+ };
36
+ /**
37
+ * Whether a granted send should be promoted to `sendRichMessage`. Fail-closed
38
+ * and class-aware: every clause must hold, otherwise the daemon keeps the HTML path.
39
+ */
40
+ export declare function shouldPromoteRich(input: {
41
+ enabled?: boolean;
42
+ send: ThreadedSend;
43
+ }): boolean;
44
+ /**
45
+ * Deliver the promoted rich message, falling back to `fallbackDeliver` (the
46
+ * unchanged HTML `sendMessage` loop) on any failure. A failure is either a
47
+ * thrown transport error or a `{ ok: false }` JSON response (the transport
48
+ * returns `res.json()` for JSON methods, so `ok:false` does not throw). On
49
+ * failure exactly one diagnostic is logged before the fallback runs; on success
50
+ * the fallback never runs.
51
+ *
52
+ * Returns the sent message's `message_id` on success (when the response carries
53
+ * one), otherwise `undefined` — including every failure/fallback path and a
54
+ * success whose response omits `result.message_id`. Callers that ignore the
55
+ * return value are unaffected.
56
+ */
57
+ export declare function deliverRichWithFallback(botApi: BotApi, base: {
58
+ chat_id: string | number;
59
+ message_thread_id?: number;
60
+ }, send: ThreadedSend, fallbackDeliver: () => Promise<void>, log?: {
61
+ warn(msg: string): void;
62
+ }): Promise<number | undefined>;
63
+ /**
64
+ * Deliver an action-needed (ask/idle) message via `sendRichMessage`, falling
65
+ * back to the unchanged HTML chunk loop on any failure. Mirrors
66
+ * {@link deliverRichWithFallback} but takes an explicit markdown body plus an
67
+ * optional top-level `reply_markup` (probe-confirmed: `sendRichMessage` accepts
68
+ * `reply_markup` alongside `rich_message`), and surfaces a structured outcome so
69
+ * the daemon can route inbound replies to the resulting message id.
70
+ *
71
+ * On rich success: returns `{ messageId, usedRich: true, usedFallback: false }`
72
+ * where `messageId` is `res.result.message_id` when present. On a `{ ok:false }`
73
+ * response or a thrown transport error: warns exactly once, runs `htmlFallback`,
74
+ * and returns `{ messageId, usedRich: false, usedFallback: true }` where
75
+ * `messageId` is the fallback's return value (the last HTML chunk's id).
76
+ */
77
+ export declare function deliverRichActionWithFallback(botApi: BotApi, base: {
78
+ chat_id: string | number;
79
+ message_thread_id?: number;
80
+ }, opts: {
81
+ markdown: string;
82
+ replyMarkup?: unknown;
83
+ requireMessageId?: boolean;
84
+ }, htmlFallback: () => Promise<number | undefined>, log?: {
85
+ warn(msg: string): void;
86
+ }): Promise<{
87
+ messageId?: number;
88
+ usedRich: boolean;
89
+ usedFallback: boolean;
90
+ }>;
@@ -9,7 +9,7 @@ export interface RunDaemonInternalDeps {
9
9
  SettingsImpl?: {
10
10
  init: (options?: {
11
11
  agentDir?: string;
12
- }) => Promise<Pick<Settings, "get" | "getAgentDir">>;
12
+ }) => Promise<Pick<Settings, "get" | "getAgentDir" | "set" | "flush">>;
13
13
  };
14
14
  DaemonImpl?: TelegramDaemonConstructor;
15
15
  processPid?: number;
@@ -18,8 +18,8 @@ export interface RunDaemonInternalDeps {
18
18
  export declare function createLightweightDaemonSettings(input: {
19
19
  agentDir: string;
20
20
  rawConfig?: unknown;
21
- }): Pick<Settings, "get" | "getAgentDir">;
22
- export declare function loadLightweightDaemonSettings(agentDir: string): Promise<Pick<Settings, "get" | "getAgentDir">>;
21
+ }): Pick<Settings, "get" | "getAgentDir" | "set" | "flush">;
22
+ export declare function loadLightweightDaemonSettings(agentDir: string): Promise<Pick<Settings, "get" | "getAgentDir" | "set" | "flush">>;
23
23
  export declare function ownerPidFromOwnerId(ownerId: string): number | undefined;
24
24
  export declare function runDaemonSmoke(opts?: {
25
25
  agentDir?: string;
@@ -231,6 +231,14 @@ export interface TelegramDaemonOptions {
231
231
  * default applies (e.g. lifecycle control disabled), no control server starts.
232
232
  */
233
233
  createLifecycleControlServer?: LifecycleControlServerFactory | null;
234
+ /** Rich text promotion (enabled by default; see rich-render.ts). */
235
+ rich?: {
236
+ enabled: boolean;
237
+ };
238
+ /** Opt-in rich-draft streaming of live turn previews (off by default; see rich-draft.ts). */
239
+ richDraft?: {
240
+ enabled: boolean;
241
+ };
234
242
  }
235
243
  interface SessionSocket {
236
244
  sessionId: string;
@@ -268,6 +276,10 @@ export declare class TelegramNotificationDaemon {
268
276
  private readonly pool;
269
277
  private readonly poller;
270
278
  private readonly dispatchState;
279
+ /** Original markdown of rich messages we sent (chat+message_id), for restoring reply context on inbound replies. */
280
+ private readonly replyStore;
281
+ /** Per-session debounce + monotonic draft-id state for opt-in draft streaming. */
282
+ private readonly draftStream;
271
283
  /** Identity-bearing sessions by repo/branch surface, used to avoid transient duplicate topics. */
272
284
  private readonly topicOwnerByIdentity;
273
285
  /** Non-identity frames held until identity creates the correct thread. */
@@ -376,7 +388,17 @@ export declare class TelegramNotificationDaemon {
376
388
  private ensureAttachmentDir;
377
389
  private cleanupAllAttachmentDirs;
378
390
  private resolveInboundAttachment;
391
+ /**
392
+ * Serialize all pool flushes. Every caller (`submitThreadedFrame`, the flat
393
+ * fallback, the drain timer's `void this.flushPool()`, topic teardown) goes
394
+ * through one promise chain, so two flushes never interleave — a live send can
395
+ * never be in-flight while a finalized flush reads `liveMessages` and decides
396
+ * to post a fresh (duplicate) final. Errors are swallowed so one failed flush
397
+ * never poisons the queue (each flush is already best-effort internally).
398
+ */
399
+ private flushChain;
379
400
  private flushPool;
401
+ private flushPoolInner;
380
402
  /**
381
403
  * Track the Telegram message id backing a streamed `(sessionId, coalesceKey)`
382
404
  * so later live/finalized frames edit it in place. Evicts this session's stale
@@ -59,6 +59,13 @@ export declare function buildActionMessage(action: {
59
59
  options?: string[];
60
60
  summary?: string;
61
61
  }): RenderedMessage;
62
+ /** Render an `action_needed` body as raw markdown (rich-message source; the HTML fallback stays on buildActionMessage). */
63
+ export declare function buildActionMarkdown(action: {
64
+ kind: "ask" | "idle";
65
+ question?: string;
66
+ options?: string[];
67
+ summary?: string;
68
+ }): string;
62
69
  /** Send Telegram HTML text chunks sequentially so long messages preserve order. */
63
70
  export declare function sendTelegramHtmlChunks(send: TelegramSend, chatId: string, text: string, inlineKeyboard?: InlineButton[][]): Promise<void>;
64
71
  /** A protocol `reply` frame the client should send to the server. */
@@ -34,6 +34,13 @@ export interface ThreadedSend {
34
34
  * message. Set for streamed turn frames so live + finalized share one message.
35
35
  */
36
36
  editable?: boolean;
37
+ /** Rich message class metadata. Only finalized final-answer sends are ever
38
+ * rich-promoted; the daemon gate (`shouldPromoteRich`) requires exactly this. */
39
+ richClass?: "final";
40
+ /** Rich final-answer markdown (raw). Delivery marker derived ONLY from a frame's `finalAnswer` bit; never inferred from `phase`. */
41
+ richMarkdown?: string;
42
+ /** Live-turn raw markdown for opt-in draft streaming (set ONLY on non-finalized turn frames; never triggers rich-final promotion, which requires `lane === "finalized"`). */
43
+ richDraftMarkdown?: string;
37
44
  }
38
45
  interface ThreadedFrame {
39
46
  type?: unknown;
@@ -50,6 +57,7 @@ interface ThreadedFrame {
50
57
  diff?: unknown;
51
58
  cwd?: unknown;
52
59
  phase?: unknown;
60
+ finalAnswer?: boolean;
53
61
  text?: unknown;
54
62
  messageRef?: unknown;
55
63
  source?: unknown;
@@ -59,6 +67,8 @@ interface ThreadedFrame {
59
67
  name?: unknown;
60
68
  verbosity?: unknown;
61
69
  redact?: unknown;
70
+ status?: unknown;
71
+ message?: unknown;
62
72
  }
63
73
  /** Format the one-time identity header as pinned bullets. */
64
74
  export declare function formatIdentityHeader(frame: {
@@ -58,11 +58,10 @@ export declare class TopicRegistry {
58
58
  markIdentitySent(sessionId: string): void;
59
59
  /** Whether the identity header still needs sending for this session. */
60
60
  needsIdentity(sessionId: string): boolean;
61
- /**
62
- * Record the topic's applied title. Returns `true` when it changed (so the
63
- * caller should `editForumTopic`), `false` when already current or unknown.
64
- */
65
- applyName(sessionId: string, name: string): boolean;
61
+ /** Whether a known session topic's applied title differs from `name`. */
62
+ needsRename(sessionId: string, name: string): boolean;
63
+ /** Commit a successfully-applied Telegram topic title. */
64
+ markNameApplied(sessionId: string, name: string): void;
66
65
  /** Remove a session topic record after Telegram deletes the topic. */
67
66
  delete(sessionId: string): boolean;
68
67
  /** Serialise for atomic persistence beside the daemon state. */
@@ -0,0 +1,7 @@
1
+ import type { AuthCredentialSelector } from "@gajae-code/ai";
2
+ export interface CliCredentialSelector {
3
+ provider?: string;
4
+ selector: AuthCredentialSelector;
5
+ raw: string;
6
+ }
7
+ export declare function parseCliCredentialSelector(raw: string): CliCredentialSelector;
@@ -1,5 +1,5 @@
1
1
  import { type AgentTelemetryConfig, type ThinkingLevel } from "@gajae-code/agent-core";
2
- import { type Model, type ProviderSessionState } from "@gajae-code/ai";
2
+ import { type AuthCredentialSelector, type Model, type ProviderSessionState } from "@gajae-code/ai";
3
3
  import { createAppendOnlyContextManager, providerSupportsAppendOnlyAuto, resolveAppendOnlyMode } from "./append-only-mode";
4
4
  import { type Rule } from "./capability/rule";
5
5
  import { ModelRegistry } from "./config/model-registry";
@@ -55,6 +55,12 @@ export interface CreateAgentSessionOptions {
55
55
  /** Optional provider-facing session identifier for prompt caches and sticky auth selection.
56
56
  * Keeps persisted session files isolated while reusing provider-side caches. */
57
57
  providerSessionId?: string;
58
+ /** Runtime credential selector for multi-account auth pools. */
59
+ credentialSelector?: {
60
+ provider?: string;
61
+ selector: AuthCredentialSelector;
62
+ raw: string;
63
+ };
58
64
  /** Custom tools to register (in addition to built-in tools). Accepts both CustomTool and ToolDefinition. */
59
65
  customTools?: (CustomTool | ToolDefinition)[];
60
66
  /** Explicit parent/phase used to load active GJC sub-skill tools for this session. */
@@ -270,6 +270,8 @@ export interface PromptOptions {
270
270
  images?: ImageContent[];
271
271
  /** When streaming, how to queue the message: "steer" (interrupt) or "followUp" (wait). */
272
272
  streamingBehavior?: "steer" | "followUp";
273
+ /** When set to "sequential", this follow-up is delivered one prompt at a time even if followUpMode is "all". */
274
+ followUpQueuePolicy?: "respect-mode" | "sequential";
273
275
  /** Optional tool choice override for the next LLM call. */
274
276
  toolChoice?: ToolChoice;
275
277
  /** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
@@ -657,7 +659,7 @@ export declare class AgentSession {
657
659
  * @throws Error if no model selected or no API key available (when not streaming)
658
660
  */
659
661
  prompt(text: string, options?: PromptOptions): Promise<void>;
660
- promptCustomMessage<T = unknown>(message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">, options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice">): Promise<void>;
662
+ promptCustomMessage<T = unknown>(message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">, options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy">): Promise<void>;
661
663
  /**
662
664
  * Queue a steering message to interrupt the agent mid-run.
663
665
  */
@@ -665,7 +667,7 @@ export declare class AgentSession {
665
667
  /**
666
668
  * Queue a follow-up message to process after the agent would otherwise stop.
667
669
  */
668
- followUp(text: string, images?: ImageContent[]): Promise<void>;
670
+ followUp(text: string, images?: ImageContent[], options?: Pick<PromptOptions, "followUpQueuePolicy">): Promise<void>;
669
671
  queueDeferredMessage(message: CustomMessage): void;
670
672
  queueDeferredMessageForTests(message: CustomMessage, triggerTurn?: boolean): void;
671
673
  /**
@@ -679,6 +681,7 @@ export declare class AgentSession {
679
681
  sendCustomMessage<T = unknown>(message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">, options?: {
680
682
  triggerTurn?: boolean;
681
683
  deliverAs?: "steer" | "followUp" | "nextTurn";
684
+ followUpQueuePolicy?: "respect-mode" | "sequential";
682
685
  }): Promise<void>;
683
686
  /** Remove undelivered queued custom messages matching `predicate` from executable queues and tagged display mirrors. */
684
687
  purgeQueuedCustomMessages(predicate: (message: CustomMessage) => boolean): PurgeQueuedCustomMessagesResult;
@@ -834,7 +837,7 @@ export declare class AgentSession {
834
837
  getAvailableModels(): Model[];
835
838
  /**
836
839
  * Set thinking level.
837
- * Saves the effective metadata-clamped level to session and settings only if it changes.
840
+ * Saves the effective metadata-clamped level to the session, and to settings when requested.
838
841
  */
839
842
  setThinkingLevel(level: ThinkingLevel | undefined, persist?: boolean): void;
840
843
  /**
@@ -53,6 +53,7 @@ export * from "./review";
53
53
  export * from "./search";
54
54
  export * from "./search-tool-bm25";
55
55
  export * from "./skill";
56
+ export * from "./skill-discovery";
56
57
  export * from "./ssh";
57
58
  export * from "./subagent";
58
59
  export * from "./telegram-send";
@@ -0,0 +1,40 @@
1
+ import type { AgentTool, AgentToolResult } from "@gajae-code/agent-core";
2
+ import * as z from "zod/v4";
3
+ import { type RuntimeSkillDiscoveryCandidate } from "../extensibility/runtime-skill-discovery";
4
+ import type { ToolSession } from ".";
5
+ declare const skillDiscoverySchema: z.ZodObject<{
6
+ query: z.ZodOptional<z.ZodString>;
7
+ source: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
8
+ all: "all";
9
+ project: "project";
10
+ user: "user";
11
+ }>>>;
12
+ limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
13
+ }, z.core.$strict>;
14
+ export type SkillDiscoveryToolInput = z.infer<typeof skillDiscoverySchema>;
15
+ export interface SkillDiscoveryToolDetails {
16
+ candidates: RuntimeSkillDiscoveryCandidate[];
17
+ count: number;
18
+ }
19
+ export declare class SkillDiscoveryTool implements AgentTool<typeof skillDiscoverySchema, SkillDiscoveryToolDetails> {
20
+ #private;
21
+ readonly name = "skill_discovery";
22
+ readonly label = "SkillDiscovery";
23
+ readonly summary = "Discover project and user runtime skills by thin metadata";
24
+ readonly loadMode = "essential";
25
+ readonly description: string;
26
+ readonly parameters: z.ZodObject<{
27
+ query: z.ZodOptional<z.ZodString>;
28
+ source: z.ZodOptional<z.ZodDefault<z.ZodEnum<{
29
+ all: "all";
30
+ project: "project";
31
+ user: "user";
32
+ }>>>;
33
+ limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
34
+ }, z.core.$strict>;
35
+ readonly strict = true;
36
+ constructor(session: ToolSession);
37
+ static createIf(session: ToolSession): SkillDiscoveryTool | null;
38
+ execute(_toolCallId: string, input: SkillDiscoveryToolInput, signal?: AbortSignal): Promise<AgentToolResult<SkillDiscoveryToolDetails>>;
39
+ }
40
+ export {};
@@ -0,0 +1,30 @@
1
+ export interface DecodePastedPathOptions {
2
+ /**
3
+ * Platform whose path semantics apply when decoding (shell unescaping,
4
+ * `file://` drive letters / UNC hosts). Defaults to `process.platform`;
5
+ * injectable so tests can pin the win32 contract from any host.
6
+ */
7
+ platform?: NodeJS.Platform;
8
+ /** Home directory for `~/` expansion. Defaults to `os.homedir()`. */
9
+ homedir?: string;
10
+ }
11
+ export interface ResolvePastedImagePathOptions extends DecodePastedPathOptions {
12
+ /** Base directory for relative paths. Defaults to `process.cwd()`. */
13
+ cwd?: string;
14
+ }
15
+ /**
16
+ * Decode pasted text into a filesystem path candidate. No filesystem access.
17
+ *
18
+ * Handles terminal drag-drop shell escaping (`\ `, `\(`, ...; skipped on
19
+ * win32 where `\` is the path separator), quoted paths, `file://` URIs
20
+ * (drive-letter, `file://localhost`, and UNC forms on win32), and `~/`
21
+ * expansion. Returns `undefined` when the text is empty, spans multiple
22
+ * lines, or is an invalid `file://` URI.
23
+ */
24
+ export declare function decodePastedPathCandidate(text: string, options?: DecodePastedPathOptions): string | undefined;
25
+ /**
26
+ * Returns the resolved path when the whole pasted text is a single path to an
27
+ * existing image file (verified by extension AND content signature),
28
+ * otherwise `undefined` (the paste is inserted as text).
29
+ */
30
+ export declare function resolvePastedImagePath(text: string, options?: ResolvePastedImagePathOptions): string | undefined;
package/package.json CHANGED
@@ -1,13 +1,10 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/coding-agent",
4
- "version": "0.9.0",
4
+ "version": "0.9.2",
5
5
  "description": "Gajae Code CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://gajae-code.com",
7
- "author": "Yeachan-Heo",
8
- "contributors": [
9
- "Mario Zechner"
10
- ],
7
+ "author": "Yeachan-Heo and Gajae Code Contributors",
11
8
  "license": "MIT",
12
9
  "repository": {
13
10
  "type": "git",
@@ -52,12 +49,12 @@
52
49
  "@agentclientprotocol/sdk": "0.21.0",
53
50
  "@babel/parser": "^7.29.3",
54
51
  "@mozilla/readability": "^0.6.0",
55
- "@gajae-code/stats": "0.9.0",
56
- "@gajae-code/agent-core": "0.9.0",
57
- "@gajae-code/ai": "0.9.0",
58
- "@gajae-code/natives": "0.9.0",
59
- "@gajae-code/tui": "0.9.0",
60
- "@gajae-code/utils": "0.9.0",
52
+ "@gajae-code/stats": "0.9.2",
53
+ "@gajae-code/agent-core": "0.9.2",
54
+ "@gajae-code/ai": "0.9.2",
55
+ "@gajae-code/natives": "0.9.2",
56
+ "@gajae-code/tui": "0.9.2",
57
+ "@gajae-code/utils": "0.9.2",
61
58
  "@puppeteer/browsers": "^2.13.0",
62
59
  "@types/turndown": "5.0.6",
63
60
  "@xterm/headless": "^6.0.0",
package/src/cli/args.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { type Effort, THINKING_EFFORTS } from "@gajae-code/ai";
5
5
  import { APP_NAME, CONFIG_DIR_NAME, logger } from "@gajae-code/utils";
6
+ import { CliParseError } from "@gajae-code/utils/cli";
6
7
  import chalk from "chalk";
7
8
  import { parseEffort } from "../thinking";
8
9
  import { BUILTIN_TOOLS } from "../tools";
@@ -20,6 +21,7 @@ export interface Args {
20
21
  mpreset?: string;
21
22
  default?: boolean;
22
23
  apiKey?: string;
24
+ credential?: string;
23
25
  systemPrompt?: string;
24
26
  appendSystemPrompt?: string;
25
27
  thinking?: Effort;
@@ -136,6 +138,12 @@ export function parseArgs(args: string[]): Args {
136
138
  result.default = true;
137
139
  } else if (arg === "--api-key" && i + 1 < args.length) {
138
140
  result.apiKey = args[++i];
141
+ } else if (arg === "--credential") {
142
+ const next = args[i + 1];
143
+ if (!next || next.startsWith("-")) {
144
+ throw new CliParseError("--credential requires <selector>");
145
+ }
146
+ result.credential = args[++i];
139
147
  } else if (arg === "--system-prompt" && i + 1 < args.length) {
140
148
  result.systemPrompt = args[++i];
141
149
  } else if (arg === "--append-system-prompt" && i + 1 < args.length) {