@mafdet/cli 0.4.3 → 0.4.5
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 +107 -0
- package/README.md +16 -8
- package/README.zh-CN.md +12 -4
- package/dist/agent/command-policy.d.ts.map +1 -1
- package/dist/agent/command-policy.js +10 -1
- package/dist/agent/context-budget.d.ts +47 -0
- package/dist/agent/context-budget.d.ts.map +1 -1
- package/dist/agent/context-budget.js +104 -0
- package/dist/agent/loop.d.ts +16 -1
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +44 -13
- package/dist/agent/session.d.ts +2 -0
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +14 -3
- package/dist/api/http.d.ts +15 -1
- package/dist/api/http.d.ts.map +1 -1
- package/dist/api/http.js +26 -5
- package/dist/api/sse-parser.d.ts +41 -1
- package/dist/api/sse-parser.d.ts.map +1 -1
- package/dist/api/sse-parser.js +90 -22
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +146 -21
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +21 -4
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +0 -2
- package/dist/commands/runtime.d.ts +6 -0
- package/dist/commands/runtime.d.ts.map +1 -1
- package/dist/commands/sessions.d.ts.map +1 -1
- package/dist/commands/sessions.js +163 -26
- package/dist/config/config.d.ts +0 -12
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +31 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/output/format.d.ts.map +1 -1
- package/dist/output/format.js +10 -5
- package/dist/output/terminal-safe.d.ts +12 -0
- package/dist/output/terminal-safe.d.ts.map +1 -0
- package/dist/output/terminal-safe.js +38 -0
- package/dist/sessions/index-file.d.ts +2 -0
- package/dist/sessions/index-file.d.ts.map +1 -1
- package/dist/sessions/index-file.js +3 -0
- package/dist/sessions/open.d.ts.map +1 -1
- package/dist/sessions/open.js +29 -3
- package/dist/sessions/recorder.d.ts.map +1 -1
- package/dist/sessions/recorder.js +6 -1
- package/dist/sessions/resume.d.ts +1 -0
- package/dist/sessions/resume.d.ts.map +1 -1
- package/dist/sessions/resume.js +5 -3
- package/dist/sessions/session-id.d.ts +16 -0
- package/dist/sessions/session-id.d.ts.map +1 -0
- package/dist/sessions/session-id.js +93 -0
- package/dist/sessions/store.d.ts +41 -2
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +48 -24
- package/dist/sessions/usage.d.ts +6 -0
- package/dist/sessions/usage.d.ts.map +1 -1
- package/dist/sessions/usage.js +12 -1
- package/dist/tui/agent-surface.d.ts.map +1 -1
- package/dist/tui/agent-surface.js +16 -3
- package/dist/tui/app.d.ts +14 -0
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +84 -17
- package/dist/tui/picker.d.ts.map +1 -1
- package/dist/tui/picker.js +2 -1
- package/dist/tui/session-centre.d.ts +5 -0
- package/dist/tui/session-centre.d.ts.map +1 -1
- package/dist/tui/session-centre.js +9 -3
- package/dist/tui/start-agent.d.ts +59 -0
- package/dist/tui/start-agent.d.ts.map +1 -1
- package/dist/tui/start-agent.js +182 -50
- package/package.json +1 -1
package/dist/api/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/api/http.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAC1C,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,QAAmB,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,IAAI,EAAE,CAAC,CAAC;IACR,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;gBAE7B,OAAO,EAAE,iBAAiB;IAShC,OAAO,CAAC,CAAC,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAKrB,QAAQ,CAAC,CAAC,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAK3B,8DAA8D;IACxD,UAAU,CACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;IAiBpC
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/api/http.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAC1C,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,QAAmB,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,IAAI,EAAE,CAAC,CAAC;IACR,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;gBAE7B,OAAO,EAAE,iBAAiB;IAShC,OAAO,CAAC,CAAC,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAKrB,QAAQ,CAAC,CAAC,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAK3B,8DAA8D;IACxD,UAAU,CACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;IAiBpC;;;;;;;;;OASG;IACG,UAAU,CACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC;QACT,QAAQ,EAAE,QAAQ,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,IAAI,CAAC;QAClB,MAAM,EAAE;YAAE,aAAa,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC;KACrD,CAAC;YA2BY,IAAI;YA4DJ,QAAQ;YAuCR,SAAS;CAexB;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,UAAU,CAAC,CAkCrB"}
|
package/dist/api/http.js
CHANGED
|
@@ -49,15 +49,36 @@ export class HttpClient {
|
|
|
49
49
|
requestId,
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Raw streaming response, for the SSE reader to consume.
|
|
54
|
+
*
|
|
55
|
+
* F-009 · Returns `abort` and the limits the reader should apply. The
|
|
56
|
+
* request timeout in `send` is cleared once the headers arrive - which for a
|
|
57
|
+
* stream is the start - so from here on the deadline belongs to whoever is
|
|
58
|
+
* reading the body, and it needs a way to close the connection rather than
|
|
59
|
+
* merely stop reading. Generation is what gets billed; dropping our end of
|
|
60
|
+
* a socket we are no longer reading does not stop it.
|
|
61
|
+
*/
|
|
53
62
|
async postStream(path, body, options = {}) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
const streamController = new AbortController();
|
|
64
|
+
const response = await this.send("POST", path, body, {
|
|
65
|
+
...options,
|
|
66
|
+
signal: options.signal
|
|
67
|
+
? AbortSignal.any([options.signal, streamController.signal])
|
|
68
|
+
: streamController.signal,
|
|
69
|
+
}, { accept: "text/event-stream" });
|
|
57
70
|
const requestId = requestIdOf(response.headers);
|
|
58
71
|
if (!response.ok)
|
|
59
72
|
throw await this.errorFrom(response, requestId, options);
|
|
60
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
response,
|
|
75
|
+
requestId,
|
|
76
|
+
abort: () => streamController.abort(),
|
|
77
|
+
limits: {
|
|
78
|
+
idleTimeoutMs: options.timeoutMs ?? this.timeoutMs,
|
|
79
|
+
maxBytes: options.maxBytes ?? this.maxBytes,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
61
82
|
}
|
|
62
83
|
async send(method, path, body, options, extraHeaders = {}) {
|
|
63
84
|
const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
package/dist/api/sse-parser.d.ts
CHANGED
|
@@ -62,11 +62,51 @@ export declare class ChatStreamAssembler {
|
|
|
62
62
|
push(payload: string): boolean;
|
|
63
63
|
result(): StreamResult;
|
|
64
64
|
}
|
|
65
|
+
/** F-009 · Limits that apply for the whole life of the stream. */
|
|
66
|
+
export interface StreamLimits {
|
|
67
|
+
/**
|
|
68
|
+
* How long to wait for the *next* chunk, not for the whole stream. A total
|
|
69
|
+
* deadline would kill a legitimately long generation; what "hung" actually
|
|
70
|
+
* means is that nothing is arriving.
|
|
71
|
+
*/
|
|
72
|
+
readonly idleTimeoutMs?: number;
|
|
73
|
+
/** Total bytes accepted before the stream is treated as hostile. */
|
|
74
|
+
readonly maxBytes?: number;
|
|
75
|
+
/**
|
|
76
|
+
* R-3 · A ceiling on the whole stream, not just on the gaps in it.
|
|
77
|
+
*
|
|
78
|
+
* The idle deadline is the right primitive for "this hung" - a long
|
|
79
|
+
* generation is not a stalled one - but it is defeated by a trickle: a few
|
|
80
|
+
* bytes before each deadline holds the connection indefinitely. The byte cap
|
|
81
|
+
* bounds the volume and not the time, so this bounds the time.
|
|
82
|
+
*
|
|
83
|
+
* Generous on purpose. It is a backstop against a hostile upstream, not a
|
|
84
|
+
* limit on how long a model may think, and a single response that takes
|
|
85
|
+
* longer than this is not a case worth serving.
|
|
86
|
+
*/
|
|
87
|
+
readonly maxTotalMs?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Closes the underlying connection. Cancelling the reader alone stops us
|
|
90
|
+
* reading; it does not reliably stop the gateway generating, and generation
|
|
91
|
+
* is what gets billed.
|
|
92
|
+
*/
|
|
93
|
+
readonly abort?: () => void;
|
|
94
|
+
}
|
|
95
|
+
export declare const DEFAULT_STREAM_IDLE_MS = 120000;
|
|
96
|
+
export declare const DEFAULT_STREAM_MAX_BYTES: number;
|
|
97
|
+
/** Matches the agent's wall-clock ceiling (§7.4), for the same reason. */
|
|
98
|
+
export declare const DEFAULT_STREAM_TOTAL_MS: number;
|
|
65
99
|
/**
|
|
66
100
|
* Read a streaming response to completion.
|
|
67
101
|
*
|
|
68
102
|
* `signal` is the user's Ctrl+C: aborting stops reading AND closes the
|
|
69
103
|
* connection, which is what actually tells the gateway to stop generating.
|
|
104
|
+
*
|
|
105
|
+
* F-009 · The request timeout in `HttpClient` is cleared the moment the
|
|
106
|
+
* response headers arrive, which for a stream is the beginning rather than the
|
|
107
|
+
* end. Without the two limits below, a gateway that sent headers and then
|
|
108
|
+
* stopped would hang the CLI forever, and one that never stopped sending would
|
|
109
|
+
* grow the buffer until the process died.
|
|
70
110
|
*/
|
|
71
|
-
export declare function readChatStream(response: Response, events?: StreamEvents, signal?: AbortSignal): Promise<StreamResult>;
|
|
111
|
+
export declare function readChatStream(response: Response, events?: StreamEvents, signal?: AbortSignal, limits?: StreamLimits): Promise<StreamResult>;
|
|
72
112
|
//# sourceMappingURL=sse-parser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse-parser.d.ts","sourceRoot":"","sources":["../../src/api/sse-parser.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sse-parser.d.ts","sourceRoot":"","sources":["../../src/api/sse-parser.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf;AAWD,+EAA+E;AAC/E,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAE7C,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE;IAMjC,kDAAkD;IAClD,GAAG,IAAI,MAAM,EAAE;IASf,OAAO,CAAC,KAAK;CAYd;AAED,8DAA8D;AAC9D,qBAAa,mBAAmB;IAOlB,OAAO,CAAC,QAAQ,CAAC,MAAM;IANnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAC7D,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,IAAI,CAAS;gBAEQ,MAAM,GAAE,YAAiB;IAEtD,4DAA4D;IAC5D,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAgD9B,MAAM,IAAI,YAAY;CASvB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,eAAO,MAAM,sBAAsB,SAAU,CAAC;AAC9C,eAAO,MAAM,wBAAwB,QAAmB,CAAC;AACzD,0EAA0E;AAC1E,eAAO,MAAM,uBAAuB,QAAc,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,QAAQ,EAClB,MAAM,GAAE,YAAiB,EACzB,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,GAAE,YAAiB,GACxB,OAAO,CAAC,YAAY,CAAC,CAyFvB"}
|
package/dist/api/sse-parser.js
CHANGED
|
@@ -1,22 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Every rule here is a fact measured in Phase 0 against real transcripts from
|
|
5
|
-
* DeepSeek, Kimi, Qwen and Vertex Gemini (tests/fixtures/cli-000*), not an
|
|
6
|
-
* assumption about how streaming "usually" works:
|
|
7
|
-
*
|
|
8
|
-
* - `id` and `name` arrive exactly once, so tool calls merge by `index`.
|
|
9
|
-
* - Qwen sends `"id": ""` on continuation frames; `??` would overwrite the
|
|
10
|
-
* real id with an empty string and the tool result would be rejected.
|
|
11
|
-
* - Gemini packs a thought signature into the id (700+ chars) and delivers a
|
|
12
|
-
* whole call in one frame, where Kimi takes nine — fragment counts mean
|
|
13
|
-
* nothing.
|
|
14
|
-
* - `finish_reason` arrives in its own frame with an empty delta; `[DONE]`
|
|
15
|
-
* ends the stream.
|
|
16
|
-
* - With `stream_options.include_usage`, usage arrives AFTER finish_reason
|
|
17
|
-
* in a frame whose `choices` is non-empty but whose delta is empty. A
|
|
18
|
-
* parser that only reads usage when `choices` is empty reports zero cost.
|
|
19
|
-
*/
|
|
1
|
+
import { ApiError } from "./errors.js";
|
|
2
|
+
import { ExitCode } from "../output/exit-codes.js";
|
|
20
3
|
/** Split a byte stream into SSE `data:` payloads across arbitrary chunking. */
|
|
21
4
|
export class SseFramer {
|
|
22
5
|
buffer = "";
|
|
@@ -117,20 +100,37 @@ export class ChatStreamAssembler {
|
|
|
117
100
|
};
|
|
118
101
|
}
|
|
119
102
|
}
|
|
103
|
+
export const DEFAULT_STREAM_IDLE_MS = 120_000;
|
|
104
|
+
export const DEFAULT_STREAM_MAX_BYTES = 32 * 1024 * 1024;
|
|
105
|
+
/** Matches the agent's wall-clock ceiling (§7.4), for the same reason. */
|
|
106
|
+
export const DEFAULT_STREAM_TOTAL_MS = 30 * 60_000;
|
|
120
107
|
/**
|
|
121
108
|
* Read a streaming response to completion.
|
|
122
109
|
*
|
|
123
110
|
* `signal` is the user's Ctrl+C: aborting stops reading AND closes the
|
|
124
111
|
* connection, which is what actually tells the gateway to stop generating.
|
|
112
|
+
*
|
|
113
|
+
* F-009 · The request timeout in `HttpClient` is cleared the moment the
|
|
114
|
+
* response headers arrive, which for a stream is the beginning rather than the
|
|
115
|
+
* end. Without the two limits below, a gateway that sent headers and then
|
|
116
|
+
* stopped would hang the CLI forever, and one that never stopped sending would
|
|
117
|
+
* grow the buffer until the process died.
|
|
125
118
|
*/
|
|
126
|
-
export async function readChatStream(response, events = {}, signal) {
|
|
119
|
+
export async function readChatStream(response, events = {}, signal, limits = {}) {
|
|
127
120
|
const framer = new SseFramer();
|
|
128
121
|
const assembler = new ChatStreamAssembler(events);
|
|
129
122
|
const body = response.body;
|
|
130
123
|
if (!body)
|
|
131
124
|
return assembler.result();
|
|
125
|
+
const idleMs = limits.idleTimeoutMs ?? DEFAULT_STREAM_IDLE_MS;
|
|
126
|
+
const maxBytes = limits.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
|
|
127
|
+
const totalMs = limits.maxTotalMs ?? DEFAULT_STREAM_TOTAL_MS;
|
|
128
|
+
const startedAt = Date.now();
|
|
129
|
+
let received = 0;
|
|
132
130
|
const reader = body.getReader();
|
|
133
131
|
const onAbort = () => void reader.cancel();
|
|
132
|
+
/** Set when `[DONE]` ended the answer while bytes may still be coming. */
|
|
133
|
+
let finished = false;
|
|
134
134
|
// Attaching a listener is not enough: a signal that aborted between the
|
|
135
135
|
// request and here would never fire one, and the read below would hang
|
|
136
136
|
// forever on a stream nobody is going to close.
|
|
@@ -144,12 +144,47 @@ export async function readChatStream(response, events = {}, signal) {
|
|
|
144
144
|
for (;;) {
|
|
145
145
|
if (signal?.aborted)
|
|
146
146
|
break;
|
|
147
|
-
|
|
147
|
+
if (Date.now() - startedAt > totalMs) {
|
|
148
|
+
limits.abort?.();
|
|
149
|
+
await reader.cancel().catch(() => { });
|
|
150
|
+
throw new ApiError({
|
|
151
|
+
kind: "timeout",
|
|
152
|
+
exitCode: ExitCode.Unavailable,
|
|
153
|
+
message: `the stream ran for longer than ${totalMs}ms`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const chunk = await withIdleDeadline(reader.read(), idleMs);
|
|
157
|
+
if (chunk === IDLE) {
|
|
158
|
+
limits.abort?.();
|
|
159
|
+
await reader.cancel().catch(() => { });
|
|
160
|
+
throw new ApiError({
|
|
161
|
+
kind: "timeout",
|
|
162
|
+
exitCode: ExitCode.Unavailable,
|
|
163
|
+
message: `the stream sent nothing for ${idleMs}ms`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const { done, value } = chunk;
|
|
148
167
|
if (done)
|
|
149
168
|
break;
|
|
169
|
+
received += value.byteLength;
|
|
170
|
+
if (received > maxBytes) {
|
|
171
|
+
limits.abort?.();
|
|
172
|
+
await reader.cancel().catch(() => { });
|
|
173
|
+
throw new ApiError({
|
|
174
|
+
kind: "too_large",
|
|
175
|
+
exitCode: ExitCode.Unavailable,
|
|
176
|
+
message: `the stream exceeded the ${maxBytes} byte limit`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
150
179
|
for (const payload of framer.push(value)) {
|
|
151
|
-
if (assembler.push(payload))
|
|
180
|
+
if (assembler.push(payload)) {
|
|
181
|
+
// R-3 · `[DONE]` means the answer is complete, not that the socket
|
|
182
|
+
// is. An upstream that keeps the connection open after it would
|
|
183
|
+
// otherwise hold ours too - releasing the reader lock in the
|
|
184
|
+
// `finally` does not close anything.
|
|
185
|
+
finished = true;
|
|
152
186
|
return assembler.result();
|
|
187
|
+
}
|
|
153
188
|
}
|
|
154
189
|
}
|
|
155
190
|
for (const payload of framer.end())
|
|
@@ -157,7 +192,40 @@ export async function readChatStream(response, events = {}, signal) {
|
|
|
157
192
|
}
|
|
158
193
|
finally {
|
|
159
194
|
signal?.removeEventListener("abort", onAbort);
|
|
195
|
+
if (finished) {
|
|
196
|
+
// Cancel first, then release: cancelling closes the body, and the abort
|
|
197
|
+
// reaches the fetch itself, which is what actually frees the socket.
|
|
198
|
+
void reader.cancel().catch(() => { });
|
|
199
|
+
limits.abort?.();
|
|
200
|
+
}
|
|
160
201
|
reader.releaseLock();
|
|
161
202
|
}
|
|
162
203
|
return assembler.result();
|
|
163
204
|
}
|
|
205
|
+
/** Sentinel for "the deadline won", distinguishable from a real chunk. */
|
|
206
|
+
const IDLE = Symbol("idle");
|
|
207
|
+
/**
|
|
208
|
+
* Race a read against the idle deadline.
|
|
209
|
+
*
|
|
210
|
+
* The timer is unref'd and always cleared: a pending timer would keep the
|
|
211
|
+
* process alive after a stream that finished normally.
|
|
212
|
+
*/
|
|
213
|
+
async function withIdleDeadline(read, idleMs) {
|
|
214
|
+
let timer;
|
|
215
|
+
try {
|
|
216
|
+
return await Promise.race([
|
|
217
|
+
read,
|
|
218
|
+
new Promise((resolve) => {
|
|
219
|
+
// Deliberately not unref'd: if a stalled stream were the only thing
|
|
220
|
+
// left, an unref'd timer would let the process exit silently instead
|
|
221
|
+
// of reporting the timeout. The finally below always clears it, so
|
|
222
|
+
// nothing is held open after a stream that ended normally.
|
|
223
|
+
timer = setTimeout(() => resolve(IDLE), idleMs);
|
|
224
|
+
}),
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
if (timer !== undefined)
|
|
229
|
+
clearTimeout(timer);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/commands/agent.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/commands/agent.ts"],"names":[],"mappings":"AAQA,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAiBvE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAmBtB,eAAO,MAAM,gBAAgB,0XAK+C,CAAC;AAE7E,eAAO,MAAM,YAAY,mgBAOmB,CAAC;AAE7C,MAAM,WAAW,OAAQ,SAAQ,SAAS;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,uBAAuB,EAAE,QAAQ,CAAC;CACrD;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,QAAQ,CAAC,CA6HnB;AAoLD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,kBAAkB,EAAE,OAAO,0JAI3E"}
|
package/dist/commands/agent.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { fetchAccessibleModels, fetchCatalog, intersectModels, } from "../api/models.js";
|
|
2
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
import { approverFor } from "../agent/approvals.js";
|
|
3
5
|
import { runAgent } from "../agent/loop.js";
|
|
4
6
|
import { runSession } from "../agent/session.js";
|
|
7
|
+
import { safeForTerminal } from "../output/terminal-safe.js";
|
|
8
|
+
import { sessionsDir } from "../config/paths.js";
|
|
9
|
+
import { keyDigest } from "../security/redact.js";
|
|
10
|
+
import { decideRecording, firstSaveNotice, NOTICE_FILE, } from "../sessions/policy.js";
|
|
11
|
+
import { SessionRecorder } from "../sessions/recorder.js";
|
|
12
|
+
import { SessionWriter } from "../sessions/store.js";
|
|
5
13
|
import { READ_ONLY_TOOLS } from "../agent/tools/read-only.js";
|
|
6
14
|
import { COMMAND_TOOLS } from "../agent/tools/run-command.js";
|
|
7
15
|
import { WRITE_TOOLS } from "../agent/tools/write.js";
|
|
@@ -11,14 +19,20 @@ import { renderJson, renderRecord } from "../output/format.js";
|
|
|
11
19
|
import { formatUsd } from "../output/money.js";
|
|
12
20
|
import { buildRuntime, reportError, requireApiKey, } from "./runtime.js";
|
|
13
21
|
const USAGE = `Usage:
|
|
14
|
-
mafdet agent "<goal>" [--model <id>] [--allow-write]
|
|
22
|
+
mafdet agent "<goal>" [--model <id>] [--allow-write] [--read-only]
|
|
15
23
|
[--allow-command "<prefix>"]... [--max-turns <n>]
|
|
16
24
|
[--max-cost-usd <n>] [--json]
|
|
25
|
+
mafdet agent Interactive session (needs a terminal)
|
|
26
|
+
[--title <text>] [--no-history]
|
|
17
27
|
|
|
18
28
|
Read-only by default. With --allow-write the agent may edit and create files,
|
|
19
|
-
and every change is shown as a diff and confirmed before it is written.
|
|
20
|
-
|
|
21
|
-
cannot write at all.
|
|
29
|
+
and every change is shown as a diff and confirmed before it is written.
|
|
30
|
+
--read-only cancels an earlier --allow-write. There is no flag that approves
|
|
31
|
+
everything in advance, and a non-interactive run cannot write at all.
|
|
32
|
+
|
|
33
|
+
An interactive session is saved on this computer; --no-history records
|
|
34
|
+
nothing and --title names it in 'mafdet sessions list'. To continue a saved
|
|
35
|
+
conversation, use 'mafdet --resume <id>'.
|
|
22
36
|
`;
|
|
23
37
|
export const READ_ONLY_PROMPT = `You are the Mafdet CLI agent working inside a user's workspace.
|
|
24
38
|
|
|
@@ -74,21 +88,36 @@ export async function agentCommand(argv, io) {
|
|
|
74
88
|
io.stderr(`mafdet agent: no goal\n${USAGE}`);
|
|
75
89
|
return ExitCode.Usage;
|
|
76
90
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
// F-007 · `decideRecording` already called this surface "interactive",
|
|
92
|
+
// so the policy claimed these runs were saved while nothing saved them.
|
|
93
|
+
const recorder = openRecorder(parsed, io, model, workspace.root, mode);
|
|
94
|
+
// R-8 · In a finally: `close()` is what writes the final index entry, so
|
|
95
|
+
// a session that threw left the listing without it.
|
|
96
|
+
let summary;
|
|
97
|
+
try {
|
|
98
|
+
summary = await runSession({
|
|
99
|
+
client: runtime.dataPlane,
|
|
100
|
+
...(recorder === undefined
|
|
101
|
+
? {}
|
|
102
|
+
: { onEvent: (event) => recorder.observe(event) }),
|
|
103
|
+
model,
|
|
104
|
+
pricing: resolved,
|
|
105
|
+
catalog,
|
|
106
|
+
systemPrompt: mode === "read-only" ? READ_ONLY_PROMPT : WRITE_PROMPT,
|
|
107
|
+
tools: buildToolset(mode, parsed.allowedCommands.length > 0),
|
|
108
|
+
toolContext: { workspace, allowedCommands: parsed.allowedCommands },
|
|
109
|
+
approver: io.approver ?? approverFor(mode, io, interactive),
|
|
110
|
+
mode,
|
|
111
|
+
allowedCommands: parsed.allowedCommands,
|
|
112
|
+
stdout: io.stdout,
|
|
113
|
+
stderr: io.stderr,
|
|
114
|
+
signal: io.signal,
|
|
115
|
+
isTty: isTty(io),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
recorder?.close();
|
|
120
|
+
}
|
|
92
121
|
io.stderr("\n" +
|
|
93
122
|
renderRecord([
|
|
94
123
|
["goals", `${summary.goals}`],
|
|
@@ -130,6 +159,72 @@ export async function agentCommand(argv, io) {
|
|
|
130
159
|
return reportError(error, io);
|
|
131
160
|
}
|
|
132
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* F-007 · A recorder for an interactive `mafdet agent`, or nothing.
|
|
164
|
+
*
|
|
165
|
+
* The same store, policy and failure behaviour as the TUI: `--no-history`
|
|
166
|
+
* writes nothing at all, and a store that cannot be opened costs the
|
|
167
|
+
* transcript rather than the session.
|
|
168
|
+
*/
|
|
169
|
+
/**
|
|
170
|
+
* R-8 · The first-save notice, once per machine.
|
|
171
|
+
*
|
|
172
|
+
* The marker file is shared with the TUI on purpose: the notice is about this
|
|
173
|
+
* computer, not about which command happened to write first.
|
|
174
|
+
*/
|
|
175
|
+
function showNoticeOnce(root, io) {
|
|
176
|
+
const marker = join(root, NOTICE_FILE);
|
|
177
|
+
try {
|
|
178
|
+
if (existsSync(marker))
|
|
179
|
+
return;
|
|
180
|
+
io.stderr(firstSaveNotice(root));
|
|
181
|
+
writeFileSync(marker, `${new Date().toISOString()}\n`, { mode: 0o600 });
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Telling the user is best effort; failing to record that we told them
|
|
185
|
+
// must not stop the session.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** Whether a terminal is on the other end of stdout. Injectable for tests. */
|
|
189
|
+
function isTty(io) {
|
|
190
|
+
return io.isTty ?? process.stdout.isTTY === true;
|
|
191
|
+
}
|
|
192
|
+
function openRecorder(parsed, io, modelId, workspaceRoot, mode) {
|
|
193
|
+
const decision = decideRecording({
|
|
194
|
+
surface: "interactive",
|
|
195
|
+
noHistory: parsed.noHistory,
|
|
196
|
+
save: false,
|
|
197
|
+
});
|
|
198
|
+
if (!decision.record)
|
|
199
|
+
return undefined;
|
|
200
|
+
const env = io.env ?? process.env;
|
|
201
|
+
const root = sessionsDir(env);
|
|
202
|
+
try {
|
|
203
|
+
const writer = SessionWriter.create({
|
|
204
|
+
root,
|
|
205
|
+
title: parsed.title ??
|
|
206
|
+
workspaceRoot.split(/[\\/]/).filter(Boolean).at(-1) ??
|
|
207
|
+
"Conversation",
|
|
208
|
+
modelId,
|
|
209
|
+
workspace: workspaceRoot,
|
|
210
|
+
mode,
|
|
211
|
+
});
|
|
212
|
+
// R-8 · Said once per machine, on whichever surface records first. The
|
|
213
|
+
// TUI showed it and this one did not, so a `mafdet agent` user could have
|
|
214
|
+
// a transcript on disk without ever being told.
|
|
215
|
+
showNoticeOnce(root, io);
|
|
216
|
+
return new SessionRecorder({
|
|
217
|
+
writer,
|
|
218
|
+
root,
|
|
219
|
+
apiKeyRef: keyDigest(env.MAFDET_API_KEY ?? ""),
|
|
220
|
+
onProblem: (message) => io.stderr(`${message}\n`),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
io.stderr(`not recording this conversation: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
133
228
|
function parseArgs(argv, io) {
|
|
134
229
|
const positional = [];
|
|
135
230
|
let model;
|
|
@@ -137,6 +232,8 @@ function parseArgs(argv, io) {
|
|
|
137
232
|
let maxCostMicroUsd;
|
|
138
233
|
let json = false;
|
|
139
234
|
let allowWrite = false;
|
|
235
|
+
let noHistory = false;
|
|
236
|
+
let title;
|
|
140
237
|
const allowedCommands = [];
|
|
141
238
|
for (let i = 0; i < argv.length; i += 1) {
|
|
142
239
|
const arg = argv[i];
|
|
@@ -157,6 +254,25 @@ function parseArgs(argv, io) {
|
|
|
157
254
|
}
|
|
158
255
|
else if (arg === "--read-only")
|
|
159
256
|
allowWrite = false;
|
|
257
|
+
else if (arg === "--no-history")
|
|
258
|
+
noHistory = true;
|
|
259
|
+
else if (arg === "--title") {
|
|
260
|
+
const entry = next();
|
|
261
|
+
if (!entry) {
|
|
262
|
+
io.stderr("mafdet agent: --title needs a value\n");
|
|
263
|
+
return ExitCode.Usage;
|
|
264
|
+
}
|
|
265
|
+
title = entry;
|
|
266
|
+
}
|
|
267
|
+
else if (arg === "--resume") {
|
|
268
|
+
// F-007 · Deliberately not implemented here, and said rather than
|
|
269
|
+
// silently ignored. Resuming has to reconcile the session's model and
|
|
270
|
+
// workspace against the current key, which the TUI has a place to do
|
|
271
|
+
// and this surface does not - and a second, weaker resume path is
|
|
272
|
+
// exactly how the two drift apart. F-003 was that drift.
|
|
273
|
+
io.stderr("mafdet agent: --resume is not available here; run 'mafdet --resume <id>'\n");
|
|
274
|
+
return ExitCode.Usage;
|
|
275
|
+
}
|
|
160
276
|
else if (arg === "--max-turns") {
|
|
161
277
|
const raw = next();
|
|
162
278
|
maxTurns = Number(raw);
|
|
@@ -198,6 +314,8 @@ function parseArgs(argv, io) {
|
|
|
198
314
|
maxTurns,
|
|
199
315
|
maxCostMicroUsd,
|
|
200
316
|
json,
|
|
317
|
+
noHistory,
|
|
318
|
+
...(title === undefined ? {} : { title }),
|
|
201
319
|
};
|
|
202
320
|
}
|
|
203
321
|
/**
|
|
@@ -229,8 +347,10 @@ function finish(report, json, io) {
|
|
|
229
347
|
io.stdout(renderJson({ ...report, costMicroUsd: report.costMicroUsd.toString() }));
|
|
230
348
|
}
|
|
231
349
|
else {
|
|
232
|
-
|
|
233
|
-
|
|
350
|
+
// R-4 · The model's answer, sanitised only when a terminal is reading it.
|
|
351
|
+
if (report.finalText) {
|
|
352
|
+
io.stdout(`${safeForTerminal(report.finalText, isTty(io))}\n`);
|
|
353
|
+
}
|
|
234
354
|
io.stderr("\n" +
|
|
235
355
|
renderRecord([
|
|
236
356
|
["status", report.stopReason],
|
|
@@ -258,6 +378,11 @@ function finish(report, json, io) {
|
|
|
258
378
|
case "max_turns":
|
|
259
379
|
case "timeout":
|
|
260
380
|
case "max_cost":
|
|
381
|
+
// F-008 · A ceiling too: the window ran out, and the report says which.
|
|
382
|
+
case "context_limit":
|
|
383
|
+
// R-5 · Asked for a ceiling we cannot enforce; stopped rather than spend
|
|
384
|
+
// past it. A limit, so it belongs with the other limits.
|
|
385
|
+
case "cost_unknown":
|
|
261
386
|
// Stopped short on purpose; the report says so rather than pretending.
|
|
262
387
|
return ExitCode.LimitReached;
|
|
263
388
|
case "rate_limited":
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/commands/chat.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/commands/chat.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAWxE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAatB,MAAM,WAAW,MAAO,SAAQ,SAAS;IACvC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,gDAAgD;IAChD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAcD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,QAAQ,CAAC,CA8BnB;AA8SD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,YAAY,GAAG,SAAS,EAC/B,KAAK,EAAE,WAAW,GACjB,MAAM,GAAG,SAAS,CASpB"}
|
package/dist/commands/chat.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { fetchCatalog } from "../api/models.js";
|
|
3
3
|
import { readChatStream } from "../api/sse-parser.js";
|
|
4
4
|
import { ApiError } from "../api/errors.js";
|
|
5
|
+
import { safeForTerminal, terminalSafeWriter, } from "../output/terminal-safe.js";
|
|
5
6
|
import { sessionsDir } from "../config/paths.js";
|
|
6
7
|
import { keyDigest } from "../security/redact.js";
|
|
7
8
|
import { upsertIndexEntry } from "../sessions/index-file.js";
|
|
@@ -196,7 +197,11 @@ function saveChat(args, io, answer, usage, requestId, model) {
|
|
|
196
197
|
model_id: args.model,
|
|
197
198
|
prompt_tokens: usage.prompt_tokens,
|
|
198
199
|
completion_tokens: usage.completion_tokens,
|
|
199
|
-
|
|
200
|
+
// F-012 · Omitted rather than zeroed when the catalogue could not
|
|
201
|
+
// price it: "we do not know" must not be recorded as "it was free".
|
|
202
|
+
...(micros === undefined
|
|
203
|
+
? {}
|
|
204
|
+
: { estimated_charge_micro_usd: micros.toString() }),
|
|
200
205
|
settlement_status: "pending",
|
|
201
206
|
api_key_ref: keyDigest(readKey(io)),
|
|
202
207
|
});
|
|
@@ -217,6 +222,10 @@ function chatTitle(prompt) {
|
|
|
217
222
|
const trimmed = line.trim();
|
|
218
223
|
return trimmed.length > 60 ? `${trimmed.slice(0, 57)}...` : trimmed;
|
|
219
224
|
}
|
|
225
|
+
/** Whether a terminal is on the other end of stdout. Injectable for tests. */
|
|
226
|
+
function isTty(io) {
|
|
227
|
+
return io.isTty ?? process.stdout.isTTY === true;
|
|
228
|
+
}
|
|
220
229
|
function readKey(io) {
|
|
221
230
|
return (io.env ?? process.env).MAFDET_API_KEY ?? "";
|
|
222
231
|
}
|
|
@@ -238,12 +247,20 @@ async function priceModel(runtime, modelId, needed) {
|
|
|
238
247
|
}
|
|
239
248
|
}
|
|
240
249
|
async function streamChat(runtime, body, args, io) {
|
|
241
|
-
const { response, requestId } = await runtime.dataPlane.postStream("/chat/completions", body, {
|
|
250
|
+
const { response, requestId, abort, limits } = await runtime.dataPlane.postStream("/chat/completions", body, {
|
|
242
251
|
signal: io.signal,
|
|
243
252
|
});
|
|
244
253
|
// Text goes to stdout as it arrives; everything else is stderr, so
|
|
245
254
|
// `mafdet chat ... > answer.txt` captures only the answer.
|
|
246
|
-
|
|
255
|
+
// F-009 · The limits travel with the stream: without them a gateway that
|
|
256
|
+
// sent headers and then went quiet would hang here indefinitely.
|
|
257
|
+
// F-010 · Sanitised only when a terminal is reading: `mafdet chat >
|
|
258
|
+
// answer.txt` and `| jq` must get the bytes the model sent.
|
|
259
|
+
const write = terminalSafeWriter(io.stdout, isTty(io));
|
|
260
|
+
const result = await readChatStream(response, { onText: write }, io.signal, {
|
|
261
|
+
...limits,
|
|
262
|
+
abort,
|
|
263
|
+
});
|
|
247
264
|
io.stdout("\n");
|
|
248
265
|
const model = await priceModel(runtime, args.model, !args.quiet || args.save);
|
|
249
266
|
if (io.signal?.aborted) {
|
|
@@ -260,7 +277,7 @@ async function streamChat(runtime, body, args, io) {
|
|
|
260
277
|
async function completeChat(runtime, body, args, io) {
|
|
261
278
|
const { data, requestId } = await runtime.dataPlane.postJson("/chat/completions", body, { signal: io.signal });
|
|
262
279
|
const answer = data.choices?.[0]?.message?.content ?? "";
|
|
263
|
-
io.stdout(`${answer}\n`);
|
|
280
|
+
io.stdout(`${safeForTerminal(answer, isTty(io))}\n`);
|
|
264
281
|
const model = await priceModel(runtime, args.model, !args.quiet || args.save);
|
|
265
282
|
summarise(args, data.usage, io, model);
|
|
266
283
|
saveChat(args, io, answer, data.usage, requestId, model);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAOnD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,QAAQ,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAOnD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,QAAQ,CAsEV"}
|
package/dist/commands/config.js
CHANGED
|
@@ -44,7 +44,6 @@ export function configCommand(argv, io) {
|
|
|
44
44
|
model_source: config.modelSource,
|
|
45
45
|
base_url: config.baseUrl,
|
|
46
46
|
backend_url: config.backendUrl,
|
|
47
|
-
approval_mode: config.approvalMode,
|
|
48
47
|
api_key: config.apiKey ? keyFingerprint(config.apiKey) : null,
|
|
49
48
|
user_config: config.sources.userConfigFound
|
|
50
49
|
? config.sources.userConfigPath
|
|
@@ -61,7 +60,6 @@ export function configCommand(argv, io) {
|
|
|
61
60
|
["model", view.model ? `${view.model} (${view.model_source})` : "<unset>"],
|
|
62
61
|
["base url", view.base_url],
|
|
63
62
|
["backend url", view.backend_url],
|
|
64
|
-
["approval mode", view.approval_mode],
|
|
65
63
|
["api key", view.api_key ?? "<unset> (set MAFDET_API_KEY)"],
|
|
66
64
|
["user config", view.user_config ?? "<none>"],
|
|
67
65
|
["project config", view.project_config ?? "<none>"],
|
|
@@ -10,6 +10,12 @@ export interface CommandIo {
|
|
|
10
10
|
/** Injected by tests; production uses global fetch. */
|
|
11
11
|
fetchImpl?: typeof fetch;
|
|
12
12
|
version?: string;
|
|
13
|
+
/**
|
|
14
|
+
* F-010 · Whether stdout is a terminal. Untrusted text is stripped of
|
|
15
|
+
* control sequences only then: a pipe or a file must receive exact bytes.
|
|
16
|
+
* Defaults to `process.stdout.isTTY`; tests set it explicitly.
|
|
17
|
+
*/
|
|
18
|
+
isTty?: boolean;
|
|
13
19
|
}
|
|
14
20
|
export interface Runtime {
|
|
15
21
|
config: LoadedConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/commands/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/commands/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uDAAuD;IACvD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,YAAY,CAAC;IACrB,sCAAsC;IACtC,SAAS,EAAE,UAAU,CAAC;IACtB,yCAAyC;IACzC,YAAY,EAAE,UAAU,CAAC;CAC1B;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGjD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAgBnD;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CASxD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,GAAG,QAAQ,CAkBnE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/commands/sessions.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/commands/sessions.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AA4BnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAgBtB,wBAAsB,eAAe,CACnC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,SAAS,GACZ,OAAO,CAAC,QAAQ,CAAC,CAUnB;AAoRD,eAAO,MAAM,eAAe,GAAI,IAAI,MAAM,KAAG,MAUX,CAAC"}
|