@polydeukes/adapter-claude-code 0.6.1 → 0.8.0
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/README.ko.md +19 -11
- package/README.md +19 -11
- package/dist/bin.d.ts +9 -0
- package/dist/bin.js +30 -0
- package/dist/hook.d.ts +51 -0
- package/dist/hook.js +142 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/init.d.ts +57 -0
- package/dist/init.js +594 -0
- package/dist/resolve-umbrella.d.ts +24 -0
- package/dist/resolve-umbrella.js +52 -0
- package/dist/session-evidence.d.ts +34 -0
- package/dist/session-evidence.js +46 -0
- package/dist/session-vocabulary.d.ts +1 -1
- package/dist/session-vocabulary.js +1 -1
- package/package.json +10 -5
- package/dist/run-adapter-path.d.ts +0 -53
- package/dist/run-adapter-path.js +0 -88
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session evidence builder — one raw PreToolUse payload in, the IR's `session` key out.
|
|
3
|
+
*
|
|
4
|
+
* Composes the two readers this package already owns: the JSONL transcript beside the
|
|
5
|
+
* payload's `transcript_path`, and the spawn sidecar derived from that same location. It
|
|
6
|
+
* opens the files those readers open and spawns nothing, so the runner receives the host's
|
|
7
|
+
* session evidence as data and needs no knowledge of the format it was read from.
|
|
8
|
+
*/
|
|
9
|
+
import type { CovenantInput } from '@polydeukes/core';
|
|
10
|
+
/** {@link sessionEvidenceFromPayload} input — one raw PreToolUse payload as a JSON string. */
|
|
11
|
+
export type SessionEvidenceFromPayloadSpec = {
|
|
12
|
+
rawPayload: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The IR's session evidence, or its absence — what one payload proves about the session it
|
|
16
|
+
* was made in.
|
|
17
|
+
*/
|
|
18
|
+
export type SessionEvidenceOutcome = CovenantInput['session'];
|
|
19
|
+
/**
|
|
20
|
+
* Build the session evidence one payload carries, or `undefined`.
|
|
21
|
+
*
|
|
22
|
+
* The two absences are two facts. No `transcript_path` is no session, and answering an
|
|
23
|
+
* empty session there would make every session-free payload register transcript-mod over
|
|
24
|
+
* nothing. A path that cannot be read is a supplier fault: the lists come back empty and
|
|
25
|
+
* `evidencePath` stays, so the runner still protects the file the host named while the
|
|
26
|
+
* witness and every history declaration see an empty session and stay shut.
|
|
27
|
+
*
|
|
28
|
+
* An unparseable payload narrows to `undefined`, leaving the parse failure to the
|
|
29
|
+
* translator's own verdict, and an unreadable transcript narrows to empty lists. A permission
|
|
30
|
+
* refusal on the sidecar directory propagates, as the channel reader's own rule has it —
|
|
31
|
+
* swallowing it would report a session that never spawned — so the caller composing this
|
|
32
|
+
* into a hook keeps it inside its fail-closed catch.
|
|
33
|
+
*/
|
|
34
|
+
export declare function sessionEvidenceFromPayload(spec: SessionEvidenceFromPayloadSpec): SessionEvidenceOutcome;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session evidence builder — one raw PreToolUse payload in, the IR's `session` key out.
|
|
3
|
+
*
|
|
4
|
+
* Composes the two readers this package already owns: the JSONL transcript beside the
|
|
5
|
+
* payload's `transcript_path`, and the spawn sidecar derived from that same location. It
|
|
6
|
+
* opens the files those readers open and spawns nothing, so the runner receives the host's
|
|
7
|
+
* session evidence as data and needs no knowledge of the format it was read from.
|
|
8
|
+
*/
|
|
9
|
+
import { sessionChannelReader } from './session-channel-reader.js';
|
|
10
|
+
import { transcriptPathFromPayload } from './session-vocabulary.js';
|
|
11
|
+
import { transcriptFromJsonlFile } from './transcript.js';
|
|
12
|
+
/**
|
|
13
|
+
* Build the session evidence one payload carries, or `undefined`.
|
|
14
|
+
*
|
|
15
|
+
* The two absences are two facts. No `transcript_path` is no session, and answering an
|
|
16
|
+
* empty session there would make every session-free payload register transcript-mod over
|
|
17
|
+
* nothing. A path that cannot be read is a supplier fault: the lists come back empty and
|
|
18
|
+
* `evidencePath` stays, so the runner still protects the file the host named while the
|
|
19
|
+
* witness and every history declaration see an empty session and stay shut.
|
|
20
|
+
*
|
|
21
|
+
* An unparseable payload narrows to `undefined`, leaving the parse failure to the
|
|
22
|
+
* translator's own verdict, and an unreadable transcript narrows to empty lists. A permission
|
|
23
|
+
* refusal on the sidecar directory propagates, as the channel reader's own rule has it —
|
|
24
|
+
* swallowing it would report a session that never spawned — so the caller composing this
|
|
25
|
+
* into a hook keeps it inside its fail-closed catch.
|
|
26
|
+
*/
|
|
27
|
+
export function sessionEvidenceFromPayload(spec) {
|
|
28
|
+
const evidencePath = transcriptPathFromPayload({ rawPayload: spec.rawPayload });
|
|
29
|
+
if (evidencePath === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
const transcript = transcriptFromJsonlFile({ path: evidencePath });
|
|
32
|
+
const sidecar = sessionChannelReader({ transcriptPath: evidencePath })('sidecar');
|
|
33
|
+
return {
|
|
34
|
+
evidencePath,
|
|
35
|
+
userMessages: (transcript?.findUserMessages() ?? []).map((message) => ({
|
|
36
|
+
text: message.text,
|
|
37
|
+
...(message.timestampMs === undefined ? {} : { timestampMs: message.timestampMs }),
|
|
38
|
+
})),
|
|
39
|
+
toolCalls: (transcript?.findToolCalls() ?? []).map((call) => ({
|
|
40
|
+
name: call.name,
|
|
41
|
+
args: call.args,
|
|
42
|
+
...(call.succeeded === undefined ? {} : { succeeded: call.succeeded }),
|
|
43
|
+
})),
|
|
44
|
+
...(sidecar === undefined ? {} : { channels: { sidecar } }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -30,6 +30,6 @@ export type TranscriptPathFromPayloadSpec = {
|
|
|
30
30
|
* (unparseable JSON, a non-object payload, a non-string field), never a throw: lost
|
|
31
31
|
* evidence leaves the dispatcher on its `noopTranscript` default, which shuts the
|
|
32
32
|
* witness valve rather than opening it. A payload this function
|
|
33
|
-
* cannot parse is still dispatched —
|
|
33
|
+
* cannot parse is still dispatched — the spawned judge owns that verdict.
|
|
34
34
|
*/
|
|
35
35
|
export declare function transcriptPathFromPayload(spec: TranscriptPathFromPayloadSpec): string | undefined;
|
|
@@ -27,7 +27,7 @@ export const COMMAND_ARGS = ['command'];
|
|
|
27
27
|
* (unparseable JSON, a non-object payload, a non-string field), never a throw: lost
|
|
28
28
|
* evidence leaves the dispatcher on its `noopTranscript` default, which shuts the
|
|
29
29
|
* witness valve rather than opening it. A payload this function
|
|
30
|
-
* cannot parse is still dispatched —
|
|
30
|
+
* cannot parse is still dispatched — the spawned judge owns that verdict.
|
|
31
31
|
*/
|
|
32
32
|
export function transcriptPathFromPayload(spec) {
|
|
33
33
|
let parsed;
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polydeukes/adapter-claude-code",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Polydeukes adapter for Claude Code — up-translates PreToolUse hook payloads into the agent-neutral covenant input IR.
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Polydeukes adapter for Claude Code — up-translates PreToolUse hook payloads into the agent-neutral covenant input IR. Beta.",
|
|
5
|
+
"author": "huskyhoochu <dfg1499@gmail.com>",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
@@ -12,6 +13,9 @@
|
|
|
12
13
|
"main": "./dist/index.js",
|
|
13
14
|
"module": "./dist/index.js",
|
|
14
15
|
"types": "./dist/index.d.ts",
|
|
16
|
+
"bin": {
|
|
17
|
+
"pdks-claude-code": "./dist/bin.js"
|
|
18
|
+
},
|
|
15
19
|
"exports": {
|
|
16
20
|
".": {
|
|
17
21
|
"types": "./dist/index.d.ts",
|
|
@@ -31,11 +35,12 @@
|
|
|
31
35
|
"devDependencies": {
|
|
32
36
|
"@types/node": "^24.0.0",
|
|
33
37
|
"typescript": "7.0.2",
|
|
34
|
-
"vitest": "^
|
|
35
|
-
"@polydeukes/core": "^0.
|
|
38
|
+
"vitest": "^5.0.0",
|
|
39
|
+
"@polydeukes/core": "^0.8.0"
|
|
36
40
|
},
|
|
37
41
|
"peerDependencies": {
|
|
38
|
-
"@polydeukes/core": "^0.
|
|
42
|
+
"@polydeukes/core": "^0.8.0",
|
|
43
|
+
"polydeukes": "^0.8.0"
|
|
39
44
|
},
|
|
40
45
|
"scripts": {
|
|
41
46
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `runAdapterPath` — the adapter path's single wiring entry point.
|
|
3
|
-
*
|
|
4
|
-
* Composes translation → injected dispatch → funnel-supplement recording so every
|
|
5
|
-
* adapter-path call leaves exactly one telemetry row when summed with downstream
|
|
6
|
-
* records. I/O lives here and only here — the translation modules stay pure.
|
|
7
|
-
*/
|
|
8
|
-
import { type DispatchOutcome } from '@polydeukes/core';
|
|
9
|
-
/**
|
|
10
|
-
* The part of a dispatch result this path reads — a structural view, not a second protocol.
|
|
11
|
-
*
|
|
12
|
-
* Derived from core's `DispatchOutcome` by `Pick`, so it cannot drift from the protocol type:
|
|
13
|
-
* a field renamed there stops compiling here. It is narrower on purpose — the entries carry
|
|
14
|
-
* a telemetry word this path never reads, and demanding it would make every dispatcher the
|
|
15
|
-
* adapter accepts name a core type its own consumers may not be able to resolve.
|
|
16
|
-
*/
|
|
17
|
-
export type DispatchAdapterView = {
|
|
18
|
-
exitCode: DispatchOutcome['exitCode'];
|
|
19
|
-
results: readonly unknown[];
|
|
20
|
-
};
|
|
21
|
-
/** {@link runAdapterPath} input — one payload, where to record, and the dispatch seam. */
|
|
22
|
-
export type RunAdapterPathSpec = {
|
|
23
|
-
/** Raw hook stdin — one PreToolUse payload as a JSON string. */
|
|
24
|
-
rawPayload: string;
|
|
25
|
-
/** Where adapter-level records append. */
|
|
26
|
-
telemetryPath: string;
|
|
27
|
-
/**
|
|
28
|
-
* Injected dispatch seam — the assembler binds the real dispatcher here.
|
|
29
|
-
*
|
|
30
|
-
* Typed by what this path reads, not by what the dispatcher returns: the supplement rule
|
|
31
|
-
* below needs the exit code and whether any covenant produced an entry. A dispatcher
|
|
32
|
-
* carrying more per entry — core's `DispatchOutcome`, which adds the telemetry word the
|
|
33
|
-
* wrapper already recorded — satisfies this structurally, so binding one costs no cast.
|
|
34
|
-
*/
|
|
35
|
-
dispatch: (stdinPayload: string) => Promise<DispatchAdapterView>;
|
|
36
|
-
/** Label for adapter-level records. Default: 'adapter-claude-code'. */
|
|
37
|
-
adapterLabel?: string;
|
|
38
|
-
};
|
|
39
|
-
/** {@link runAdapterPath} result — the exit code the hook process leaves with. */
|
|
40
|
-
export type AdapterPathOutcome = {
|
|
41
|
-
exitCode: 0 | 2;
|
|
42
|
-
};
|
|
43
|
-
/**
|
|
44
|
-
* Run one PreToolUse payload through the adapter path.
|
|
45
|
-
*
|
|
46
|
-
* Fail-closed on the verdict axis: an unparseable payload, a classification failure,
|
|
47
|
-
* or a rejecting dispatch all resolve to `{ exitCode: 2 }` with one adapter `blocked`
|
|
48
|
-
* record — never a thrown error (an unhandled rejection would exit the hook
|
|
49
|
-
* non-blocking, a bypass vector). The funnel supplement is the exact rule
|
|
50
|
-
* `exitCode 0 && results.length 0 → one adapter passed record`; every other outcome
|
|
51
|
-
* appends nothing because downstream already recorded, so nothing double-counts.
|
|
52
|
-
*/
|
|
53
|
-
export declare function runAdapterPath(spec: RunAdapterPathSpec): Promise<AdapterPathOutcome>;
|
package/dist/run-adapter-path.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `runAdapterPath` — the adapter path's single wiring entry point.
|
|
3
|
-
*
|
|
4
|
-
* Composes translation → injected dispatch → funnel-supplement recording so every
|
|
5
|
-
* adapter-path call leaves exactly one telemetry row when summed with downstream
|
|
6
|
-
* records. I/O lives here and only here — the translation modules stay pure.
|
|
7
|
-
*/
|
|
8
|
-
import { readFileSync } from 'node:fs';
|
|
9
|
-
import { appendRecordFailOpen, EXIT_BREAK_BLOCKING, EXIT_UPHOLD, } from '@polydeukes/core';
|
|
10
|
-
import { collectFileChanges } from './file-changes.js';
|
|
11
|
-
import { buildCovenantInput } from './up-translate.js';
|
|
12
|
-
/** Default label for adapter-level telemetry records. */
|
|
13
|
-
const DEFAULT_ADAPTER_LABEL = 'adapter-claude-code';
|
|
14
|
-
/**
|
|
15
|
-
* Real-fs pre-state reader for fileChanges — `null` only for true absence (ENOENT).
|
|
16
|
-
*
|
|
17
|
-
* Any other read failure (permissions, a directory target, fd exhaustion) throws:
|
|
18
|
-
* `null` is the IR's creation sentinel, and a poisoned `pre: null` on an existing
|
|
19
|
-
* file would let a path-family discipline uphold the overwrite (fail-open). The
|
|
20
|
-
* caller converts the throw into one adapter `blocked` record.
|
|
21
|
-
*/
|
|
22
|
-
function readPreStateFromDisk(filePath) {
|
|
23
|
-
try {
|
|
24
|
-
return readFileSync(filePath, 'utf-8');
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
if (error.code === 'ENOENT')
|
|
28
|
-
return null;
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Run one PreToolUse payload through the adapter path.
|
|
34
|
-
*
|
|
35
|
-
* Fail-closed on the verdict axis: an unparseable payload, a classification failure,
|
|
36
|
-
* or a rejecting dispatch all resolve to `{ exitCode: 2 }` with one adapter `blocked`
|
|
37
|
-
* record — never a thrown error (an unhandled rejection would exit the hook
|
|
38
|
-
* non-blocking, a bypass vector). The funnel supplement is the exact rule
|
|
39
|
-
* `exitCode 0 && results.length 0 → one adapter passed record`; every other outcome
|
|
40
|
-
* appends nothing because downstream already recorded, so nothing double-counts.
|
|
41
|
-
*/
|
|
42
|
-
export async function runAdapterPath(spec) {
|
|
43
|
-
const label = spec.adapterLabel ?? DEFAULT_ADAPTER_LABEL;
|
|
44
|
-
const blockAndRecord = () => {
|
|
45
|
-
appendRecordFailOpen(spec.telemetryPath, { event: 'blocked', label, subject: '-' });
|
|
46
|
-
return { exitCode: EXIT_BREAK_BLOCKING };
|
|
47
|
-
};
|
|
48
|
-
let payload;
|
|
49
|
-
try {
|
|
50
|
-
payload = JSON.parse(spec.rawPayload);
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return blockAndRecord();
|
|
54
|
-
}
|
|
55
|
-
const built = buildCovenantInput([payload]);
|
|
56
|
-
if (built.ok !== true) {
|
|
57
|
-
return blockAndRecord();
|
|
58
|
-
}
|
|
59
|
-
// Attach pre/post evidence to the call it belongs to — this path translates
|
|
60
|
-
// exactly one payload, so the one evidence rides toolCalls[0]. Attached
|
|
61
|
-
// only when provable: a non-mutating payload leaves its call unproven. A pre-state
|
|
62
|
-
// read failure that is not absence blocks: evidence that cannot be gathered must not
|
|
63
|
-
// dispatch a shape that reads as creation.
|
|
64
|
-
let evidence;
|
|
65
|
-
try {
|
|
66
|
-
evidence = collectFileChanges(payload, readPreStateFromDisk);
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return blockAndRecord();
|
|
70
|
-
}
|
|
71
|
-
const input = evidence === null
|
|
72
|
-
? built.value
|
|
73
|
-
: {
|
|
74
|
-
...built.value,
|
|
75
|
-
toolCalls: built.value.toolCalls.map((call, index) => index === 0 ? { ...call, fileChange: evidence } : call),
|
|
76
|
-
};
|
|
77
|
-
let outcome;
|
|
78
|
-
try {
|
|
79
|
-
outcome = await spec.dispatch(JSON.stringify(input));
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return blockAndRecord();
|
|
83
|
-
}
|
|
84
|
-
if (outcome.exitCode === EXIT_UPHOLD && outcome.results.length === 0) {
|
|
85
|
-
appendRecordFailOpen(spec.telemetryPath, { event: 'passed', label, subject: '-' });
|
|
86
|
-
}
|
|
87
|
-
return { exitCode: outcome.exitCode };
|
|
88
|
-
}
|