hammoc 1.5.0 → 1.6.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.md +8 -2
- package/package.json +2 -2
- package/packages/client/dist/assets/{agentExampleHighlight-BgwTm15v.js → agentExampleHighlight-ltj9ce0U.js} +1 -1
- package/packages/client/dist/assets/{commandTokenHighlight-BljHwnrK.js → commandTokenHighlight-ji_ViMb4.js} +1 -1
- package/packages/client/dist/assets/{index-D3LxqW3f.js → index-B-DiRGuz.js} +1 -1
- package/packages/client/dist/assets/index-B09doO8H.js +139 -0
- package/packages/client/dist/assets/{index-NqJdhlek.js → index-BT4RIi0U.js} +535 -510
- package/packages/client/dist/assets/index-DyNJ5jEW.css +32 -0
- package/packages/client/dist/assets/{snippetTokenHighlight-DWsaQXX0.js → snippetTokenHighlight-CP3v4o2g.js} +1 -1
- package/packages/client/dist/index.html +2 -2
- package/packages/client/dist/sw.js +1 -1
- package/packages/server/dist/controllers/bmadCoreConfigController.d.ts +41 -0
- package/packages/server/dist/controllers/bmadCoreConfigController.d.ts.map +1 -0
- package/packages/server/dist/controllers/bmadCoreConfigController.js +172 -0
- package/packages/server/dist/controllers/bmadCoreConfigController.js.map +1 -0
- package/packages/server/dist/controllers/contextBuilderController.d.ts +43 -0
- package/packages/server/dist/controllers/contextBuilderController.d.ts.map +1 -0
- package/packages/server/dist/controllers/contextBuilderController.js +159 -0
- package/packages/server/dist/controllers/contextBuilderController.js.map +1 -0
- package/packages/server/dist/controllers/harnessAgentController.d.ts +7 -0
- package/packages/server/dist/controllers/harnessAgentController.d.ts.map +1 -1
- package/packages/server/dist/controllers/harnessAgentController.js +33 -0
- package/packages/server/dist/controllers/harnessAgentController.js.map +1 -1
- package/packages/server/dist/controllers/harnessBundleController.d.ts +37 -0
- package/packages/server/dist/controllers/harnessBundleController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessBundleController.js +312 -0
- package/packages/server/dist/controllers/harnessBundleController.js.map +1 -0
- package/packages/server/dist/controllers/harnessCommandController.d.ts +7 -0
- package/packages/server/dist/controllers/harnessCommandController.d.ts.map +1 -1
- package/packages/server/dist/controllers/harnessCommandController.js +33 -0
- package/packages/server/dist/controllers/harnessCommandController.js.map +1 -1
- package/packages/server/dist/controllers/harnessHookController.d.ts.map +1 -1
- package/packages/server/dist/controllers/harnessHookController.js +44 -1
- package/packages/server/dist/controllers/harnessHookController.js.map +1 -1
- package/packages/server/dist/controllers/harnessMcpController.d.ts.map +1 -1
- package/packages/server/dist/controllers/harnessMcpController.js +62 -1
- package/packages/server/dist/controllers/harnessMcpController.js.map +1 -1
- package/packages/server/dist/controllers/harnessShareScopeController.d.ts +9 -0
- package/packages/server/dist/controllers/harnessShareScopeController.d.ts.map +1 -1
- package/packages/server/dist/controllers/harnessShareScopeController.js +48 -1
- package/packages/server/dist/controllers/harnessShareScopeController.js.map +1 -1
- package/packages/server/dist/controllers/marketplaceController.d.ts +19 -0
- package/packages/server/dist/controllers/marketplaceController.d.ts.map +1 -0
- package/packages/server/dist/controllers/marketplaceController.js +74 -0
- package/packages/server/dist/controllers/marketplaceController.js.map +1 -0
- package/packages/server/dist/controllers/observabilityController.d.ts +32 -0
- package/packages/server/dist/controllers/observabilityController.d.ts.map +1 -0
- package/packages/server/dist/controllers/observabilityController.js +148 -0
- package/packages/server/dist/controllers/observabilityController.js.map +1 -0
- package/packages/server/dist/handlers/streamCallbacks.d.ts +8 -0
- package/packages/server/dist/handlers/streamCallbacks.d.ts.map +1 -1
- package/packages/server/dist/handlers/streamCallbacks.js +8 -0
- package/packages/server/dist/handlers/streamCallbacks.js.map +1 -1
- package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
- package/packages/server/dist/handlers/websocket.js +24 -2
- package/packages/server/dist/handlers/websocket.js.map +1 -1
- package/packages/server/dist/routes/harness.d.ts.map +1 -1
- package/packages/server/dist/routes/harness.js +58 -0
- package/packages/server/dist/routes/harness.js.map +1 -1
- package/packages/server/dist/services/bmadCoreConfigService.d.ts +86 -0
- package/packages/server/dist/services/bmadCoreConfigService.d.ts.map +1 -0
- package/packages/server/dist/services/bmadCoreConfigService.js +175 -0
- package/packages/server/dist/services/bmadCoreConfigService.js.map +1 -0
- package/packages/server/dist/services/bmadStatusService.d.ts +9 -0
- package/packages/server/dist/services/bmadStatusService.d.ts.map +1 -1
- package/packages/server/dist/services/bmadStatusService.js +59 -6
- package/packages/server/dist/services/bmadStatusService.js.map +1 -1
- package/packages/server/dist/services/chatService.js +1 -1
- package/packages/server/dist/services/chatService.js.map +1 -1
- package/packages/server/dist/services/contextBuilderScriptTemplate.d.ts +24 -0
- package/packages/server/dist/services/contextBuilderScriptTemplate.d.ts.map +1 -0
- package/packages/server/dist/services/contextBuilderScriptTemplate.js +181 -0
- package/packages/server/dist/services/contextBuilderScriptTemplate.js.map +1 -0
- package/packages/server/dist/services/contextBuilderService.d.ts +68 -0
- package/packages/server/dist/services/contextBuilderService.d.ts.map +1 -0
- package/packages/server/dist/services/contextBuilderService.js +345 -0
- package/packages/server/dist/services/contextBuilderService.js.map +1 -0
- package/packages/server/dist/services/fileWatcherService.d.ts.map +1 -1
- package/packages/server/dist/services/fileWatcherService.js +40 -0
- package/packages/server/dist/services/fileWatcherService.js.map +1 -1
- package/packages/server/dist/services/harnessAgentService.d.ts +18 -0
- package/packages/server/dist/services/harnessAgentService.d.ts.map +1 -1
- package/packages/server/dist/services/harnessAgentService.js +55 -0
- package/packages/server/dist/services/harnessAgentService.js.map +1 -1
- package/packages/server/dist/services/harnessBundleService.d.ts +145 -0
- package/packages/server/dist/services/harnessBundleService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessBundleService.js +1318 -0
- package/packages/server/dist/services/harnessBundleService.js.map +1 -0
- package/packages/server/dist/services/harnessCommandService.d.ts +21 -0
- package/packages/server/dist/services/harnessCommandService.d.ts.map +1 -1
- package/packages/server/dist/services/harnessCommandService.js +64 -0
- package/packages/server/dist/services/harnessCommandService.js.map +1 -1
- package/packages/server/dist/services/harnessHookService.d.ts +27 -0
- package/packages/server/dist/services/harnessHookService.d.ts.map +1 -1
- package/packages/server/dist/services/harnessHookService.js +52 -0
- package/packages/server/dist/services/harnessHookService.js.map +1 -1
- package/packages/server/dist/services/harnessMcpService.d.ts +24 -1
- package/packages/server/dist/services/harnessMcpService.d.ts.map +1 -1
- package/packages/server/dist/services/harnessMcpService.js +70 -0
- package/packages/server/dist/services/harnessMcpService.js.map +1 -1
- package/packages/server/dist/services/harnessShareScopeService.d.ts +19 -0
- package/packages/server/dist/services/harnessShareScopeService.d.ts.map +1 -1
- package/packages/server/dist/services/harnessShareScopeService.js +65 -0
- package/packages/server/dist/services/harnessShareScopeService.js.map +1 -1
- package/packages/server/dist/services/issueService.d.ts.map +1 -1
- package/packages/server/dist/services/issueService.js +1 -0
- package/packages/server/dist/services/issueService.js.map +1 -1
- package/packages/server/dist/services/marketplaceService.d.ts +50 -0
- package/packages/server/dist/services/marketplaceService.d.ts.map +1 -0
- package/packages/server/dist/services/marketplaceService.js +326 -0
- package/packages/server/dist/services/marketplaceService.js.map +1 -0
- package/packages/server/dist/services/observabilityService.d.ts +87 -0
- package/packages/server/dist/services/observabilityService.d.ts.map +1 -0
- package/packages/server/dist/services/observabilityService.js +0 -0
- package/packages/server/dist/services/observabilityService.js.map +1 -0
- package/packages/server/dist/services/queueService.d.ts.map +1 -1
- package/packages/server/dist/services/queueService.js +3 -0
- package/packages/server/dist/services/queueService.js.map +1 -1
- package/packages/server/dist/services/sessionService.d.ts +16 -0
- package/packages/server/dist/services/sessionService.d.ts.map +1 -1
- package/packages/server/dist/services/sessionService.js +125 -0
- package/packages/server/dist/services/sessionService.js.map +1 -1
- package/packages/server/dist/services/tokenCountService.d.ts +71 -0
- package/packages/server/dist/services/tokenCountService.d.ts.map +1 -0
- package/packages/server/dist/services/tokenCountService.js +313 -0
- package/packages/server/dist/services/tokenCountService.js.map +1 -0
- package/packages/server/dist/snippets/apply-qa-fixes +7 -5
- package/packages/server/dist/snippets/qa-review +5 -1
- package/packages/server/dist/utils/assertSafeBundlePath.d.ts +29 -0
- package/packages/server/dist/utils/assertSafeBundlePath.d.ts.map +1 -0
- package/packages/server/dist/utils/assertSafeBundlePath.js +53 -0
- package/packages/server/dist/utils/assertSafeBundlePath.js.map +1 -0
- package/packages/server/dist/utils/bundledBinaryModelSupport.d.ts +7 -0
- package/packages/server/dist/utils/bundledBinaryModelSupport.d.ts.map +1 -0
- package/packages/server/dist/utils/bundledBinaryModelSupport.js +107 -0
- package/packages/server/dist/utils/bundledBinaryModelSupport.js.map +1 -0
- package/packages/server/dist/utils/effortUtils.d.ts +2 -2
- package/packages/server/dist/utils/effortUtils.js +5 -5
- package/packages/server/dist/utils/effortUtils.js.map +1 -1
- package/packages/server/dist/utils/errors.d.ts +1 -0
- package/packages/server/dist/utils/errors.d.ts.map +1 -1
- package/packages/server/dist/utils/errors.js +17 -0
- package/packages/server/dist/utils/errors.js.map +1 -1
- package/packages/server/dist/utils/harnessBundleSchema.d.ts +14 -12
- package/packages/server/dist/utils/harnessBundleSchema.d.ts.map +1 -1
- package/packages/server/dist/utils/harnessBundleSchema.js +11 -1
- package/packages/server/dist/utils/harnessBundleSchema.js.map +1 -1
- package/packages/server/dist/utils/harnessPaths.d.ts +40 -0
- package/packages/server/dist/utils/harnessPaths.d.ts.map +1 -1
- package/packages/server/dist/utils/harnessPaths.js +123 -0
- package/packages/server/dist/utils/harnessPaths.js.map +1 -1
- package/packages/server/package.json +2 -1
- package/packages/server/resources/internals/INDEX.md +3 -1
- package/packages/server/resources/internals/bmad-qa-fix-marker.md +32 -0
- package/packages/server/resources/internals/harness-files.md +22 -0
- package/packages/server/resources/internals/observability-storage.md +23 -0
- package/packages/server/resources/manual/02-chat.md +2 -2
- package/packages/server/resources/manual/05-projects.md +3 -1
- package/packages/server/resources/manual/10-project-board.md +4 -3
- package/packages/server/resources/manual/11-bmad-method-integration.md +10 -8
- package/packages/server/resources/manual/12-harness-workbench.md +82 -1
- package/packages/server/resources/manual/13-settings.md +4 -4
- package/packages/shared/dist/index.d.ts +4 -0
- package/packages/shared/dist/index.d.ts.map +1 -1
- package/packages/shared/dist/index.js +8 -0
- package/packages/shared/dist/index.js.map +1 -1
- package/packages/shared/dist/types/bmadCoreConfig.d.ts +71 -0
- package/packages/shared/dist/types/bmadCoreConfig.d.ts.map +1 -0
- package/packages/shared/dist/types/bmadCoreConfig.js +30 -0
- package/packages/shared/dist/types/bmadCoreConfig.js.map +1 -0
- package/packages/shared/dist/types/bmadStatus.d.ts +10 -0
- package/packages/shared/dist/types/bmadStatus.d.ts.map +1 -1
- package/packages/shared/dist/types/bmadStatus.js.map +1 -1
- package/packages/shared/dist/types/board.d.ts +6 -0
- package/packages/shared/dist/types/board.d.ts.map +1 -1
- package/packages/shared/dist/types/contextBuilder.d.ts +102 -0
- package/packages/shared/dist/types/contextBuilder.d.ts.map +1 -0
- package/packages/shared/dist/types/contextBuilder.js +55 -0
- package/packages/shared/dist/types/contextBuilder.js.map +1 -0
- package/packages/shared/dist/types/harnessBundle.d.ts +35 -0
- package/packages/shared/dist/types/harnessBundle.d.ts.map +1 -1
- package/packages/shared/dist/types/marketplace.d.ts +83 -0
- package/packages/shared/dist/types/marketplace.d.ts.map +1 -0
- package/packages/shared/dist/types/marketplace.js +18 -0
- package/packages/shared/dist/types/marketplace.js.map +1 -0
- package/packages/shared/dist/types/observability.d.ts +148 -0
- package/packages/shared/dist/types/observability.d.ts.map +1 -0
- package/packages/shared/dist/types/observability.js +24 -0
- package/packages/shared/dist/types/observability.js.map +1 -0
- package/packages/shared/dist/types/preferences.d.ts +2 -0
- package/packages/shared/dist/types/preferences.d.ts.map +1 -1
- package/packages/shared/dist/types/preferences.js.map +1 -1
- package/packages/shared/dist/types/sdk.d.ts +1 -1
- package/packages/shared/dist/types/sdk.d.ts.map +1 -1
- package/packages/shared/dist/types/sdk.js +1 -1
- package/packages/shared/dist/types/sdk.js.map +1 -1
- package/packages/client/dist/assets/index-CjyjnXB8.css +0 -32
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story 31.3 (Task A.3 / A.5): token attribution + exact count_tokens proxy.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities, sharing one text-resolution path so the "exact count"
|
|
5
|
+
* input and the "approximation" input are always the same bytes (N-1):
|
|
6
|
+
*
|
|
7
|
+
* 1. listTokenAttribution(projectSlug) — enumerate the measured harness
|
|
8
|
+
* elements (project/global CLAUDE.md · each skill's SKILL.md · the
|
|
9
|
+
* Hammoc-managed context-builder injection) and report UTF-8 byte size +
|
|
10
|
+
* `approxTokens = ceil(bytes/4)` + a sha256 content hash (AC-B1 / AC-B2).
|
|
11
|
+
*
|
|
12
|
+
* 2. exactCount(projectSlug, req) — proxy the official `count_tokens` via the
|
|
13
|
+
* already-installed `@anthropic-ai/sdk` `messages.countTokens` (spike #2,
|
|
14
|
+
* §15 — no new dependency), keyed by a server-recomputed content hash
|
|
15
|
+
* (N-B: the request hash is only an optimistic hint). Failures are
|
|
16
|
+
* non-blocking (`failed: true`) so the client keeps the approximation
|
|
17
|
+
* (AC-B3.c).
|
|
18
|
+
*
|
|
19
|
+
* Enumeration REUSES existing controllers/services (claudeMdService,
|
|
20
|
+
* harnessSkillService, contextBuilderService) — no new file scanner (S-2). The
|
|
21
|
+
* context-builder injection is estimated from the Story 31.2 manifest WITHOUT
|
|
22
|
+
* executing the hook (dynamic variable / command values are placeholders).
|
|
23
|
+
*
|
|
24
|
+
* Per spike #1 (§14) the SERVER approximation tier is byte `size/4` only — the
|
|
25
|
+
* `@anthropic-ai/tokenizer` tier was NOT adopted (Task A.4 skipped).
|
|
26
|
+
*/
|
|
27
|
+
import { type TokenAttributionItem, type ExactTokenCountRequest, type ExactTokenCountResponse } from '@hammoc/shared';
|
|
28
|
+
/** ceil(bytes / 4) — the size-based heuristic (§14). */
|
|
29
|
+
export declare function approxTokensFromBytes(bytes: number): number;
|
|
30
|
+
declare class TokenCountService {
|
|
31
|
+
/** sha256(text) → token count. In-memory; persisted best-effort to disk. */
|
|
32
|
+
private cache;
|
|
33
|
+
private cacheLoaded;
|
|
34
|
+
private cacheFilePath;
|
|
35
|
+
private loadCache;
|
|
36
|
+
private persistCache;
|
|
37
|
+
/**
|
|
38
|
+
* Enumerate measured harness elements (AC-B1.a). Missing CLAUDE.md files and a
|
|
39
|
+
* disabled/empty context builder are omitted (they contribute no tokens).
|
|
40
|
+
*/
|
|
41
|
+
listTokenAttribution(projectSlug: string): Promise<TokenAttributionItem[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Exact count for one element (AC-B3). The text is re-resolved server-side and
|
|
44
|
+
* its sha256 is the AUTHORITATIVE cache key (N-B) — the request `contentHash`
|
|
45
|
+
* is only an optimistic hint. count_tokens failures return `failed: true`.
|
|
46
|
+
*/
|
|
47
|
+
exactCount(projectSlug: string, req: ExactTokenCountRequest): Promise<ExactTokenCountResponse>;
|
|
48
|
+
private makeItem;
|
|
49
|
+
/** Resolve the text used for BOTH attribution and exact-count (N-1). */
|
|
50
|
+
private resolveElementText;
|
|
51
|
+
private readClaudeMd;
|
|
52
|
+
/**
|
|
53
|
+
* Read a skill's SKILL.md, validating `elementPath` against the enumerated
|
|
54
|
+
* skills so an arbitrary client path can never be read (the path must be one
|
|
55
|
+
* the server itself vouched for in listTokenAttribution).
|
|
56
|
+
*/
|
|
57
|
+
private readSkillTextByPath;
|
|
58
|
+
/**
|
|
59
|
+
* Assemble the context-builder `additionalContext` text WITHOUT executing the
|
|
60
|
+
* hook (N-1). Reference files are read (their content dominates the size);
|
|
61
|
+
* dynamic variables / acknowledged commands contribute only their block
|
|
62
|
+
* headers with a `(dynamic)` placeholder. Mirrors the block format of
|
|
63
|
+
* `contextBuilderScriptTemplate` for the statically-knowable parts. Returns ''
|
|
64
|
+
* when the builder is disabled or empty.
|
|
65
|
+
*/
|
|
66
|
+
private assembleContextBuilderText;
|
|
67
|
+
private callCountTokens;
|
|
68
|
+
}
|
|
69
|
+
export declare const tokenCountService: TokenCountService;
|
|
70
|
+
export {};
|
|
71
|
+
//# sourceMappingURL=tokenCountService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenCountService.d.ts","sourceRoot":"","sources":["../../src/services/tokenCountService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAQH,OAAO,EACL,KAAK,oBAAoB,EAEzB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC7B,MAAM,gBAAgB,CAAC;AAwBxB,wDAAwD;AACxD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE3D;AA6BD,cAAM,iBAAiB;IACrB,4EAA4E;IAC5E,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,WAAW,CAAS;IAE5B,OAAO,CAAC,aAAa;YAIP,SAAS;YAcT,YAAY;IAU1B;;;OAGG;IACG,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAkChF;;;;OAIG;IACG,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAqBpG,OAAO,CAAC,QAAQ;IAiBhB,wEAAwE;YAC1D,kBAAkB;YAqBlB,YAAY;IAW1B;;;;OAIG;YACW,mBAAmB;IAkBjC;;;;;;;OAOG;YACW,0BAA0B;YAwC1B,eAAe;CAiB9B;AAED,eAAO,MAAM,iBAAiB,mBAA0B,CAAC"}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story 31.3 (Task A.3 / A.5): token attribution + exact count_tokens proxy.
|
|
3
|
+
*
|
|
4
|
+
* Two responsibilities, sharing one text-resolution path so the "exact count"
|
|
5
|
+
* input and the "approximation" input are always the same bytes (N-1):
|
|
6
|
+
*
|
|
7
|
+
* 1. listTokenAttribution(projectSlug) — enumerate the measured harness
|
|
8
|
+
* elements (project/global CLAUDE.md · each skill's SKILL.md · the
|
|
9
|
+
* Hammoc-managed context-builder injection) and report UTF-8 byte size +
|
|
10
|
+
* `approxTokens = ceil(bytes/4)` + a sha256 content hash (AC-B1 / AC-B2).
|
|
11
|
+
*
|
|
12
|
+
* 2. exactCount(projectSlug, req) — proxy the official `count_tokens` via the
|
|
13
|
+
* already-installed `@anthropic-ai/sdk` `messages.countTokens` (spike #2,
|
|
14
|
+
* §15 — no new dependency), keyed by a server-recomputed content hash
|
|
15
|
+
* (N-B: the request hash is only an optimistic hint). Failures are
|
|
16
|
+
* non-blocking (`failed: true`) so the client keeps the approximation
|
|
17
|
+
* (AC-B3.c).
|
|
18
|
+
*
|
|
19
|
+
* Enumeration REUSES existing controllers/services (claudeMdService,
|
|
20
|
+
* harnessSkillService, contextBuilderService) — no new file scanner (S-2). The
|
|
21
|
+
* context-builder injection is estimated from the Story 31.2 manifest WITHOUT
|
|
22
|
+
* executing the hook (dynamic variable / command values are placeholders).
|
|
23
|
+
*
|
|
24
|
+
* Per spike #1 (§14) the SERVER approximation tier is byte `size/4` only — the
|
|
25
|
+
* `@anthropic-ai/tokenizer` tier was NOT adopted (Task A.4 skipped).
|
|
26
|
+
*/
|
|
27
|
+
import fs from 'node:fs/promises';
|
|
28
|
+
import { readFileSync } from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import os from 'node:os';
|
|
31
|
+
import { createHash } from 'node:crypto';
|
|
32
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
33
|
+
import { createLogger } from '../utils/logger.js';
|
|
34
|
+
import { claudeMdService } from './claudeMdService.js';
|
|
35
|
+
import { harnessSkillService } from './harnessSkillService.js';
|
|
36
|
+
import { contextBuilderService } from './contextBuilderService.js';
|
|
37
|
+
import { projectService } from './projectService.js';
|
|
38
|
+
const log = createLogger('tokenCountService');
|
|
39
|
+
/**
|
|
40
|
+
* Model passed to count_tokens. The count is tokenizer-family stable across
|
|
41
|
+
* Claude 4.x, so any current model works; overridable via env in case the
|
|
42
|
+
* default is later deprecated. spike #2 verified the call live.
|
|
43
|
+
*/
|
|
44
|
+
const COUNT_TOKENS_MODEL = process.env.OBSERVABILITY_COUNT_TOKENS_MODEL || 'claude-sonnet-4-6';
|
|
45
|
+
function sha256(text) {
|
|
46
|
+
return createHash('sha256').update(text, 'utf8').digest('hex');
|
|
47
|
+
}
|
|
48
|
+
function utf8Bytes(text) {
|
|
49
|
+
return Buffer.byteLength(text, 'utf8');
|
|
50
|
+
}
|
|
51
|
+
/** ceil(bytes / 4) — the size-based heuristic (§14). */
|
|
52
|
+
export function approxTokensFromBytes(bytes) {
|
|
53
|
+
return Math.ceil(Math.max(0, bytes) / 4);
|
|
54
|
+
}
|
|
55
|
+
function samePath(a, b) {
|
|
56
|
+
const na = path.normalize(a);
|
|
57
|
+
const nb = path.normalize(b);
|
|
58
|
+
return process.platform === 'win32' ? na.toLowerCase() === nb.toLowerCase() : na === nb;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Construct an Anthropic client for count_tokens (spike #2). Priority:
|
|
62
|
+
* `ANTHROPIC_API_KEY` → the Claude Code OAuth access token from
|
|
63
|
+
* `~/.claude/.credentials.json` (verified live). Returns null when neither is
|
|
64
|
+
* available — the caller degrades gracefully.
|
|
65
|
+
*/
|
|
66
|
+
function getAnthropicClient() {
|
|
67
|
+
if (process.env.ANTHROPIC_API_KEY)
|
|
68
|
+
return new Anthropic();
|
|
69
|
+
try {
|
|
70
|
+
const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
71
|
+
const cred = JSON.parse(readFileSync(credPath, 'utf8'));
|
|
72
|
+
const token = cred?.claudeAiOauth?.accessToken;
|
|
73
|
+
if (token)
|
|
74
|
+
return new Anthropic({ authToken: token });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// no credentials — fall through.
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
class TokenCountService {
|
|
82
|
+
/** sha256(text) → token count. In-memory; persisted best-effort to disk. */
|
|
83
|
+
cache = new Map();
|
|
84
|
+
cacheLoaded = false;
|
|
85
|
+
cacheFilePath() {
|
|
86
|
+
return path.join(os.homedir(), '.hammoc', 'observability', 'token-count-cache.json');
|
|
87
|
+
}
|
|
88
|
+
async loadCache() {
|
|
89
|
+
if (this.cacheLoaded)
|
|
90
|
+
return;
|
|
91
|
+
this.cacheLoaded = true;
|
|
92
|
+
try {
|
|
93
|
+
const text = await fs.readFile(this.cacheFilePath(), 'utf8');
|
|
94
|
+
const obj = JSON.parse(text);
|
|
95
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
96
|
+
if (typeof v === 'number')
|
|
97
|
+
this.cache.set(k, v);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// missing/corrupt — start empty.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async persistCache() {
|
|
105
|
+
try {
|
|
106
|
+
const file = this.cacheFilePath();
|
|
107
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
108
|
+
await fs.writeFile(file, JSON.stringify(Object.fromEntries(this.cache)), 'utf8');
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
log.warn(`token cache persist failed: ${err.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Enumerate measured harness elements (AC-B1.a). Missing CLAUDE.md files and a
|
|
116
|
+
* disabled/empty context builder are omitted (they contribute no tokens).
|
|
117
|
+
*/
|
|
118
|
+
async listTokenAttribution(projectSlug) {
|
|
119
|
+
const items = [];
|
|
120
|
+
// project / global CLAUDE.md
|
|
121
|
+
const projectMd = await this.readClaudeMd({ scope: 'project', projectSlug });
|
|
122
|
+
if (projectMd)
|
|
123
|
+
items.push(this.makeItem('claudeMd-project', 'CLAUDE.md (project)', projectMd.text, projectMd.path));
|
|
124
|
+
const globalMd = await this.readClaudeMd({ scope: 'user' });
|
|
125
|
+
if (globalMd)
|
|
126
|
+
items.push(this.makeItem('claudeMd-global', 'CLAUDE.md (global)', globalMd.text, globalMd.path));
|
|
127
|
+
// each skill's SKILL.md (active source only)
|
|
128
|
+
try {
|
|
129
|
+
const { cards } = await harnessSkillService.listCards(projectSlug);
|
|
130
|
+
for (const card of cards) {
|
|
131
|
+
const root = card.sources[0]?.absoluteRoot;
|
|
132
|
+
if (!root)
|
|
133
|
+
continue;
|
|
134
|
+
const skillMd = path.join(root, 'SKILL.md');
|
|
135
|
+
try {
|
|
136
|
+
const text = await fs.readFile(skillMd, 'utf8');
|
|
137
|
+
items.push(this.makeItem('skill', `skill: ${card.name}`, text, skillMd));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// SKILL.md unreadable — skip this card.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
log.warn(`skill enumeration failed: ${err.message}`);
|
|
146
|
+
}
|
|
147
|
+
// Hammoc-managed context builder injection (manifest-assembled, no execution)
|
|
148
|
+
const cbText = await this.assembleContextBuilderText(projectSlug);
|
|
149
|
+
if (cbText)
|
|
150
|
+
items.push(this.makeItem('contextBuilder', 'Context builder injection', cbText));
|
|
151
|
+
return items;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Exact count for one element (AC-B3). The text is re-resolved server-side and
|
|
155
|
+
* its sha256 is the AUTHORITATIVE cache key (N-B) — the request `contentHash`
|
|
156
|
+
* is only an optimistic hint. count_tokens failures return `failed: true`.
|
|
157
|
+
*/
|
|
158
|
+
async exactCount(projectSlug, req) {
|
|
159
|
+
const text = await this.resolveElementText(projectSlug, req.kind, req.path);
|
|
160
|
+
if (text == null)
|
|
161
|
+
return { tokens: 0, cached: false, failed: true };
|
|
162
|
+
const key = sha256(text);
|
|
163
|
+
await this.loadCache();
|
|
164
|
+
const hit = this.cache.get(key);
|
|
165
|
+
if (hit !== undefined)
|
|
166
|
+
return { tokens: hit, cached: true };
|
|
167
|
+
const tokens = await this.callCountTokens(text);
|
|
168
|
+
if (tokens == null)
|
|
169
|
+
return { tokens: 0, cached: false, failed: true };
|
|
170
|
+
this.cache.set(key, tokens);
|
|
171
|
+
void this.persistCache();
|
|
172
|
+
return { tokens, cached: false };
|
|
173
|
+
}
|
|
174
|
+
// -------------------------------------------------------------------------
|
|
175
|
+
// internals
|
|
176
|
+
// -------------------------------------------------------------------------
|
|
177
|
+
makeItem(kind, label, text, filePath) {
|
|
178
|
+
const bytes = utf8Bytes(text);
|
|
179
|
+
return {
|
|
180
|
+
kind,
|
|
181
|
+
label,
|
|
182
|
+
...(filePath ? { path: filePath } : {}),
|
|
183
|
+
bytes,
|
|
184
|
+
approxTokens: approxTokensFromBytes(bytes),
|
|
185
|
+
contentHash: sha256(text),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Resolve the text used for BOTH attribution and exact-count (N-1). */
|
|
189
|
+
async resolveElementText(projectSlug, kind, elementPath) {
|
|
190
|
+
switch (kind) {
|
|
191
|
+
case 'claudeMd-project':
|
|
192
|
+
return (await this.readClaudeMd({ scope: 'project', projectSlug }))?.text ?? null;
|
|
193
|
+
case 'claudeMd-global':
|
|
194
|
+
return (await this.readClaudeMd({ scope: 'user' }))?.text ?? null;
|
|
195
|
+
case 'contextBuilder': {
|
|
196
|
+
const t = await this.assembleContextBuilderText(projectSlug);
|
|
197
|
+
return t || null;
|
|
198
|
+
}
|
|
199
|
+
case 'skill':
|
|
200
|
+
return this.readSkillTextByPath(projectSlug, elementPath);
|
|
201
|
+
default:
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async readClaudeMd(ref) {
|
|
206
|
+
try {
|
|
207
|
+
const res = await claudeMdService.read(ref);
|
|
208
|
+
return { text: res.content ?? '', path: res.absolutePath ?? 'CLAUDE.md' };
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return null; // missing / not-a-file → omit.
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Read a skill's SKILL.md, validating `elementPath` against the enumerated
|
|
216
|
+
* skills so an arbitrary client path can never be read (the path must be one
|
|
217
|
+
* the server itself vouched for in listTokenAttribution).
|
|
218
|
+
*/
|
|
219
|
+
async readSkillTextByPath(projectSlug, elementPath) {
|
|
220
|
+
if (!elementPath)
|
|
221
|
+
return null;
|
|
222
|
+
try {
|
|
223
|
+
const { cards } = await harnessSkillService.listCards(projectSlug);
|
|
224
|
+
for (const card of cards) {
|
|
225
|
+
const root = card.sources[0]?.absoluteRoot;
|
|
226
|
+
if (!root)
|
|
227
|
+
continue;
|
|
228
|
+
const skillMd = path.join(root, 'SKILL.md');
|
|
229
|
+
if (samePath(skillMd, elementPath)) {
|
|
230
|
+
return await fs.readFile(skillMd, 'utf8');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// fall through.
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Assemble the context-builder `additionalContext` text WITHOUT executing the
|
|
241
|
+
* hook (N-1). Reference files are read (their content dominates the size);
|
|
242
|
+
* dynamic variables / acknowledged commands contribute only their block
|
|
243
|
+
* headers with a `(dynamic)` placeholder. Mirrors the block format of
|
|
244
|
+
* `contextBuilderScriptTemplate` for the statically-knowable parts. Returns ''
|
|
245
|
+
* when the builder is disabled or empty.
|
|
246
|
+
*/
|
|
247
|
+
async assembleContextBuilderText(projectSlug) {
|
|
248
|
+
let manifest;
|
|
249
|
+
try {
|
|
250
|
+
({ manifest } = await contextBuilderService.readManifest(projectSlug));
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return '';
|
|
254
|
+
}
|
|
255
|
+
if (!manifest.enabled)
|
|
256
|
+
return '';
|
|
257
|
+
let projectRoot;
|
|
258
|
+
try {
|
|
259
|
+
projectRoot = await projectService.resolveOriginalPath(projectSlug);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return '';
|
|
263
|
+
}
|
|
264
|
+
const blocks = [];
|
|
265
|
+
for (const rel of manifest.files) {
|
|
266
|
+
const abs = path.resolve(projectRoot, rel);
|
|
267
|
+
try {
|
|
268
|
+
const content = await fs.readFile(abs, 'utf8');
|
|
269
|
+
blocks.push(`## Reference file: ${rel}\n\n${content}`);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
blocks.push(`## Reference file: ${rel}\n\n(파일을 찾을 수 없음: ${rel})`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Dynamic variable headers (values are computed at hook runtime — placeholder here).
|
|
276
|
+
if (manifest.variables.gitBranch)
|
|
277
|
+
blocks.push('## Current git branch\n\n(dynamic)');
|
|
278
|
+
if (manifest.variables.recentCommits) {
|
|
279
|
+
blocks.push(`## Recent ${manifest.recentCommitsCount ?? 5} commit(s)\n\n(dynamic)`);
|
|
280
|
+
}
|
|
281
|
+
if (manifest.variables.uncommittedCount)
|
|
282
|
+
blocks.push('## Uncommitted file count\n\n(dynamic)');
|
|
283
|
+
if (manifest.variables.today)
|
|
284
|
+
blocks.push('## Today\n\n(dynamic)');
|
|
285
|
+
if (manifest.variables.activeBmadStory)
|
|
286
|
+
blocks.push('## Active BMad story\n\n(dynamic)');
|
|
287
|
+
for (const cc of manifest.customCommands) {
|
|
288
|
+
if (cc.acknowledged)
|
|
289
|
+
blocks.push(`## Command: ${cc.command}\n\n(dynamic)`);
|
|
290
|
+
}
|
|
291
|
+
return blocks.join('\n\n');
|
|
292
|
+
}
|
|
293
|
+
async callCountTokens(text) {
|
|
294
|
+
const client = getAnthropicClient();
|
|
295
|
+
if (!client) {
|
|
296
|
+
log.warn('count_tokens unavailable — no ANTHROPIC_API_KEY and no OAuth credentials');
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const res = await client.messages.countTokens({
|
|
301
|
+
model: COUNT_TOKENS_MODEL,
|
|
302
|
+
messages: [{ role: 'user', content: text }],
|
|
303
|
+
});
|
|
304
|
+
return res.input_tokens;
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
log.warn(`count_tokens failed: ${err.message}`);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
export const tokenCountService = new TokenCountService();
|
|
313
|
+
//# sourceMappingURL=tokenCountService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokenCountService.js","sourceRoot":"","sources":["../../src/services/tokenCountService.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
/BMad:agents:dev
|
|
2
|
-
---
|
|
3
|
-
*review-qa {arg1}
|
|
4
|
-
|
|
5
|
-
After
|
|
1
|
+
/BMad:agents:dev
|
|
2
|
+
---
|
|
3
|
+
*review-qa {arg1}
|
|
4
|
+
|
|
5
|
+
After the fixes are applied and you update the story's Dev Agent Record, record which QA gate you addressed so Hammoc can track it. Read the `updated:` value from this story's QA gate YAML, then append this exact line inside the Completion Notes — replace the placeholder with that value. Do NOT change Status and do NOT edit the gate file:
|
|
6
|
+
<!-- hammoc:qa-fix gate="<paste the gate's updated value>" applied="true" -->
|
|
7
|
+
This flips the story from "apply QA fixes" to "request QA re-review" in Hammoc.
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
/BMad:agents:qa
|
|
2
2
|
---
|
|
3
|
-
*review {arg1}
|
|
3
|
+
*review {arg1}
|
|
4
|
+
|
|
5
|
+
When you write the quality gate, if the gate decision is CONCERNS or FAIL, also record it in the story so Hammoc tracks that a fix is pending. Append this exact line inside the story's QA Results section, replacing the placeholder with the gate's `updated:` value you just wrote:
|
|
6
|
+
<!-- hammoc:qa-fix gate="<paste the gate's updated value>" applied="false" -->
|
|
7
|
+
For a PASS or WAIVED gate, do not add this marker.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story 30.5 (AC9): single-source guard against ZIP-slip / path-traversal
|
|
3
|
+
* during harness bundle import.
|
|
4
|
+
*
|
|
5
|
+
* `jszip` does not normalise entry paths — a malicious bundle can ship an
|
|
6
|
+
* entry called `../../etc/passwd` or `C:\Windows\System32\evil.dll` and naive
|
|
7
|
+
* code that joins it to the project root would write outside the target.
|
|
8
|
+
* Every entry path read from an import ZIP must therefore pass through this
|
|
9
|
+
* function before the bundle service touches disk.
|
|
10
|
+
*
|
|
11
|
+
* Rejected shapes:
|
|
12
|
+
* - absolute POSIX (`/etc/...`)
|
|
13
|
+
* - absolute Windows (`C:\...`, `C:/...`)
|
|
14
|
+
* - UNC (`\\server\share`, `//server/share`)
|
|
15
|
+
* - any traversal component (`..` segment anywhere in the path)
|
|
16
|
+
* - embedded null byte (would terminate the path early in fs APIs)
|
|
17
|
+
*
|
|
18
|
+
* Accepted shapes:
|
|
19
|
+
* - POSIX-style relative paths (`skills/foo/SKILL.md`)
|
|
20
|
+
* - Windows-style backslashes are normalised, then validated
|
|
21
|
+
* - the empty string (the zip "/" root) is rejected as it carries no entry
|
|
22
|
+
*/
|
|
23
|
+
export declare class UnsafeBundlePathError extends Error {
|
|
24
|
+
readonly code = "HARNESS_BUNDLE_UNSAFE_PATH";
|
|
25
|
+
readonly relativePath: string;
|
|
26
|
+
constructor(relativePath: string, reason: string);
|
|
27
|
+
}
|
|
28
|
+
export declare function assertSafeBundlePath(relativePath: string): void;
|
|
29
|
+
//# sourceMappingURL=assertSafeBundlePath.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertSafeBundlePath.d.ts","sourceRoot":"","sources":["../../src/utils/assertSafeBundlePath.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,IAAI,gCAAgC;IAC7C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAClB,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAIjD;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAmB/D"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story 30.5 (AC9): single-source guard against ZIP-slip / path-traversal
|
|
3
|
+
* during harness bundle import.
|
|
4
|
+
*
|
|
5
|
+
* `jszip` does not normalise entry paths — a malicious bundle can ship an
|
|
6
|
+
* entry called `../../etc/passwd` or `C:\Windows\System32\evil.dll` and naive
|
|
7
|
+
* code that joins it to the project root would write outside the target.
|
|
8
|
+
* Every entry path read from an import ZIP must therefore pass through this
|
|
9
|
+
* function before the bundle service touches disk.
|
|
10
|
+
*
|
|
11
|
+
* Rejected shapes:
|
|
12
|
+
* - absolute POSIX (`/etc/...`)
|
|
13
|
+
* - absolute Windows (`C:\...`, `C:/...`)
|
|
14
|
+
* - UNC (`\\server\share`, `//server/share`)
|
|
15
|
+
* - any traversal component (`..` segment anywhere in the path)
|
|
16
|
+
* - embedded null byte (would terminate the path early in fs APIs)
|
|
17
|
+
*
|
|
18
|
+
* Accepted shapes:
|
|
19
|
+
* - POSIX-style relative paths (`skills/foo/SKILL.md`)
|
|
20
|
+
* - Windows-style backslashes are normalised, then validated
|
|
21
|
+
* - the empty string (the zip "/" root) is rejected as it carries no entry
|
|
22
|
+
*/
|
|
23
|
+
const ABSOLUTE_WIN_RE = /^[A-Za-z]:[\\/]/;
|
|
24
|
+
const TRAVERSAL_SEGMENT_RE = /(^|[\\/])\.\.([\\/]|$)/;
|
|
25
|
+
export class UnsafeBundlePathError extends Error {
|
|
26
|
+
code = 'HARNESS_BUNDLE_UNSAFE_PATH';
|
|
27
|
+
relativePath;
|
|
28
|
+
constructor(relativePath, reason) {
|
|
29
|
+
super(`unsafe bundle entry "${relativePath}": ${reason}`);
|
|
30
|
+
this.relativePath = relativePath;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function assertSafeBundlePath(relativePath) {
|
|
34
|
+
if (typeof relativePath !== 'string' || relativePath.length === 0) {
|
|
35
|
+
throw new UnsafeBundlePathError(String(relativePath), 'empty entry path');
|
|
36
|
+
}
|
|
37
|
+
if (relativePath.includes('\0')) {
|
|
38
|
+
throw new UnsafeBundlePathError(relativePath, 'null byte in entry path');
|
|
39
|
+
}
|
|
40
|
+
if (relativePath.startsWith('/') || relativePath.startsWith('\\')) {
|
|
41
|
+
throw new UnsafeBundlePathError(relativePath, 'absolute path not allowed');
|
|
42
|
+
}
|
|
43
|
+
if (ABSOLUTE_WIN_RE.test(relativePath)) {
|
|
44
|
+
throw new UnsafeBundlePathError(relativePath, 'Windows-absolute path not allowed');
|
|
45
|
+
}
|
|
46
|
+
if (relativePath.startsWith('\\\\') || relativePath.startsWith('//')) {
|
|
47
|
+
throw new UnsafeBundlePathError(relativePath, 'UNC path not allowed');
|
|
48
|
+
}
|
|
49
|
+
if (TRAVERSAL_SEGMENT_RE.test(relativePath)) {
|
|
50
|
+
throw new UnsafeBundlePathError(relativePath, 'parent-directory traversal not allowed');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=assertSafeBundlePath.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertSafeBundlePath.js","sourceRoot":"","sources":["../../src/utils/assertSafeBundlePath.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAC1C,MAAM,oBAAoB,GAAG,wBAAwB,CAAC;AAEtD,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IACrC,IAAI,GAAG,4BAA4B,CAAC;IACpC,YAAY,CAAS;IAC9B,YAAY,YAAoB,EAAE,MAAc;QAC9C,KAAK,CAAC,wBAAwB,YAAY,MAAM,MAAM,EAAE,CAAC,CAAC;QAC1D,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACnC,CAAC;CACF;AAED,MAAM,UAAU,oBAAoB,CAAC,YAAoB;IACvD,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,qBAAqB,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC5E,CAAC;IACD,IAAI,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,qBAAqB,CAAC,YAAY,EAAE,yBAAyB,CAAC,CAAC;IAC3E,CAAC;IACD,IAAI,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,qBAAqB,CAAC,YAAY,EAAE,2BAA2B,CAAC,CAAC;IAC7E,CAAC;IACD,IAAI,eAAe,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,qBAAqB,CAAC,YAAY,EAAE,mCAAmC,CAAC,CAAC;IACrF,CAAC;IACD,IAAI,YAAY,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,qBAAqB,CAAC,YAAY,EAAE,sBAAsB,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,oBAAoB,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QAC5C,MAAM,IAAI,qBAAqB,CAAC,YAAY,EAAE,wCAAwC,CAAC,CAAC;IAC1F,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when `model` is a known native-1M model but the bundled binary does NOT
|
|
3
|
+
* recognize it — so its 1M context silently falls back to ~200K. Conservative:
|
|
4
|
+
* returns false (no warning) for bare aliases, unreadable binaries, or uncertainty.
|
|
5
|
+
*/
|
|
6
|
+
export declare function modelMissingNative1MSupport(model?: string): Promise<boolean>;
|
|
7
|
+
//# sourceMappingURL=bundledBinaryModelSupport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundledBinaryModelSupport.d.ts","sourceRoot":"","sources":["../../src/utils/bundledBinaryModelSupport.ts"],"names":[],"mappings":"AA8EA;;;;GAIG;AACH,wBAAsB,2BAA2B,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAgBlF"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects whether the bundled Claude Code engine (the platform binary shipped
|
|
3
|
+
* inside @anthropic-ai/claude-agent-sdk-<platform>-<arch>) actually recognizes a
|
|
4
|
+
* given model id.
|
|
5
|
+
*
|
|
6
|
+
* Native 1M context is opted into via the `[1m]` model suffix, but the binary
|
|
7
|
+
* only honors it for models baked into its own model table. If the selected model
|
|
8
|
+
* is newer than the bundled binary, the suffix is silently ignored and the model
|
|
9
|
+
* falls back to ~200K — which makes long sessions auto-compact far below their
|
|
10
|
+
* real limit and can break resume (thinking-block signature rejection). This util
|
|
11
|
+
* lets the server surface that silent fallback instead of failing mysteriously.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
import { createRequire } from 'module';
|
|
17
|
+
import { isNative1MModel } from '@hammoc/shared';
|
|
18
|
+
import { createLogger } from './logger.js';
|
|
19
|
+
const log = createLogger('bundledBinaryModelSupport');
|
|
20
|
+
/** Claude model ids embedded in the binary, e.g. claude-opus-4-8, claude-sonnet-4-6, claude-opus-4-20250514. */
|
|
21
|
+
const MODEL_ID_RE = /claude-(?:opus|sonnet|haiku)-[0-9]+(?:-[0-9]+)*/g;
|
|
22
|
+
let cache = null;
|
|
23
|
+
let scanPromise = null;
|
|
24
|
+
/** Resolve the platform binary path (claude.exe on Windows, claude elsewhere), or null if not installed. */
|
|
25
|
+
function resolveBundledBinaryPath() {
|
|
26
|
+
const pkg = `@anthropic-ai/claude-agent-sdk-${os.platform()}-${os.arch()}`;
|
|
27
|
+
const exe = os.platform() === 'win32' ? 'claude.exe' : 'claude';
|
|
28
|
+
try {
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const pkgJsonPath = require.resolve(`${pkg}/package.json`);
|
|
31
|
+
return path.join(path.dirname(pkgJsonPath), exe);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Stream the (large) binary in chunks and collect every Claude model id it contains. */
|
|
38
|
+
function scanBinaryForModels(binPath) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const models = new Set();
|
|
41
|
+
let tail = '';
|
|
42
|
+
const stream = fs.createReadStream(binPath, { encoding: 'latin1', highWaterMark: 1 << 22 });
|
|
43
|
+
stream.on('data', (chunk) => {
|
|
44
|
+
const text = tail + chunk;
|
|
45
|
+
const matches = text.match(MODEL_ID_RE);
|
|
46
|
+
if (matches)
|
|
47
|
+
for (const m of matches)
|
|
48
|
+
models.add(m);
|
|
49
|
+
// keep a small overlap so ids straddling a chunk boundary still match
|
|
50
|
+
tail = text.slice(-64);
|
|
51
|
+
});
|
|
52
|
+
stream.on('end', () => resolve(models));
|
|
53
|
+
stream.on('error', (err) => {
|
|
54
|
+
log.warn(`binary model scan failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
55
|
+
resolve(new Set());
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Model ids the bundled binary recognizes (memoized by path+mtime). Empty set if unreadable. */
|
|
60
|
+
async function getRecognizedModels() {
|
|
61
|
+
const binPath = resolveBundledBinaryPath();
|
|
62
|
+
if (!binPath)
|
|
63
|
+
return new Set();
|
|
64
|
+
let mtimeMs = 0;
|
|
65
|
+
try {
|
|
66
|
+
mtimeMs = fs.statSync(binPath).mtimeMs;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return new Set();
|
|
70
|
+
}
|
|
71
|
+
if (cache && cache.path === binPath && cache.mtimeMs === mtimeMs)
|
|
72
|
+
return cache.models;
|
|
73
|
+
if (!scanPromise) {
|
|
74
|
+
scanPromise = scanBinaryForModels(binPath).then((models) => {
|
|
75
|
+
cache = { path: binPath, mtimeMs, models };
|
|
76
|
+
scanPromise = null;
|
|
77
|
+
return models;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return scanPromise;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* True when `model` is a known native-1M model but the bundled binary does NOT
|
|
84
|
+
* recognize it — so its 1M context silently falls back to ~200K. Conservative:
|
|
85
|
+
* returns false (no warning) for bare aliases, unreadable binaries, or uncertainty.
|
|
86
|
+
*/
|
|
87
|
+
export async function modelMissingNative1MSupport(model) {
|
|
88
|
+
if (!model)
|
|
89
|
+
return false;
|
|
90
|
+
const base = model.replace(/\[1m\]$/i, '');
|
|
91
|
+
// Only fully-versioned ids are checkable against the binary; bare aliases like
|
|
92
|
+
// 'opus'/'sonnet' resolve to a concrete model server-side, so skip them.
|
|
93
|
+
if (!/^claude-(?:opus|sonnet|haiku)-[0-9]/.test(base))
|
|
94
|
+
return false;
|
|
95
|
+
if (!isNative1MModel(base))
|
|
96
|
+
return false;
|
|
97
|
+
const recognized = await getRecognizedModels();
|
|
98
|
+
if (recognized.size === 0)
|
|
99
|
+
return false; // couldn't read the binary → don't false-warn
|
|
100
|
+
for (const r of recognized) {
|
|
101
|
+
// exact, or either is the date-suffixed snapshot of the other (suffix always starts with '-')
|
|
102
|
+
if (r === base || r.startsWith(`${base}-`) || base.startsWith(`${r}-`))
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=bundledBinaryModelSupport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundledBinaryModelSupport.js","sourceRoot":"","sources":["../../src/utils/bundledBinaryModelSupport.ts"],"names":[],"mappings":"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"}
|