@polydeukes/adapter-claude-code 0.6.1 → 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.
@@ -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 — `runAdapterPath` owns that verdict.
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 — `runAdapterPath` owns that verdict.
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@polydeukes/adapter-claude-code",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "description": "Polydeukes adapter for Claude Code — up-translates PreToolUse hook payloads into the agent-neutral covenant input IR. Alpha.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -12,6 +12,9 @@
12
12
  "main": "./dist/index.js",
13
13
  "module": "./dist/index.js",
14
14
  "types": "./dist/index.d.ts",
15
+ "bin": {
16
+ "pdks-claude-code": "./dist/bin.js"
17
+ },
15
18
  "exports": {
16
19
  ".": {
17
20
  "types": "./dist/index.d.ts",
@@ -31,11 +34,12 @@
31
34
  "devDependencies": {
32
35
  "@types/node": "^24.0.0",
33
36
  "typescript": "7.0.2",
34
- "vitest": "^4.1.0",
35
- "@polydeukes/core": "^0.6.1"
37
+ "vitest": "^5.0.0",
38
+ "@polydeukes/core": "^0.7.1"
36
39
  },
37
40
  "peerDependencies": {
38
- "@polydeukes/core": "^0.6.1"
41
+ "@polydeukes/core": "^0.7.1",
42
+ "polydeukes": "^0.7.1"
39
43
  },
40
44
  "scripts": {
41
45
  "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>;
@@ -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
- }