@ishlabs/cli 0.12.2 → 0.14.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/dist/commands/chat-config.d.ts +23 -0
- package/dist/commands/chat-config.js +289 -0
- package/dist/commands/chat.js +26 -37
- package/dist/commands/iteration.js +219 -22
- package/dist/commands/profile.js +75 -9
- package/dist/commands/source.js +6 -4
- package/dist/commands/study-analyze.d.ts +41 -0
- package/dist/commands/study-analyze.js +187 -0
- package/dist/commands/study-run.js +359 -30
- package/dist/commands/study-screenshots.d.ts +20 -0
- package/dist/commands/study-screenshots.js +216 -0
- package/dist/commands/study.js +174 -9
- package/dist/commands/workspace.js +35 -2
- package/dist/lib/accessibility-profile.d.ts +12 -0
- package/dist/lib/accessibility-profile.js +136 -0
- package/dist/lib/alias-store.d.ts +1 -0
- package/dist/lib/alias-store.js +1 -0
- package/dist/lib/ask-questions.js +9 -0
- package/dist/lib/billing.d.ts +55 -0
- package/dist/lib/billing.js +77 -0
- package/dist/lib/command-helpers.d.ts +6 -0
- package/dist/lib/command-helpers.js +12 -0
- package/dist/lib/docs.js +1181 -38
- package/dist/lib/enums.d.ts +54 -0
- package/dist/lib/enums.js +100 -0
- package/dist/lib/local-sim/actions.d.ts +2 -1
- package/dist/lib/local-sim/actions.js +88 -13
- package/dist/lib/local-sim/loop.js +49 -19
- package/dist/lib/local-sim/tabs.d.ts +27 -0
- package/dist/lib/local-sim/tabs.js +157 -0
- package/dist/lib/local-sim/types.d.ts +15 -0
- package/dist/lib/modality.d.ts +70 -1
- package/dist/lib/modality.js +323 -17
- package/dist/lib/output.js +61 -4
- package/dist/lib/skill-content.js +397 -19
- package/dist/lib/types.d.ts +6 -1
- package/dist/lib/types.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side validator for the AccessibilityProfile v1.0 JSONB shape on
|
|
3
|
+
* TesterProfile.accessibility_profile. Mirrors
|
|
4
|
+
* `ish-mcp/spec/accessibility-profile-schema.v1.json` (additionalProperties:
|
|
5
|
+
* false at every level except `extensions`). An empty object `{}` is the
|
|
6
|
+
* canonical default. When non-empty, `version` is required and must be
|
|
7
|
+
* `"1.0"`.
|
|
8
|
+
*
|
|
9
|
+
* Surfacing validation here is cheaper than a server round-trip and gives
|
|
10
|
+
* agents the same exit-2 error contract they get for other CLI inputs.
|
|
11
|
+
*/
|
|
12
|
+
const SPEC_URL = "https://ishlabs.io/spec/accessibility-profile-schema.v1.json";
|
|
13
|
+
const TEXT_SIZE = new Set(["default", "large", "xl", "xxl"]);
|
|
14
|
+
const CONTRAST_PREFERENCE = new Set(["default", "more", "less"]);
|
|
15
|
+
const COLOR_SCHEME = new Set(["no_preference", "light", "dark"]);
|
|
16
|
+
const COLOR_FILTER = new Set([
|
|
17
|
+
"none",
|
|
18
|
+
"deuteranopia",
|
|
19
|
+
"protanopia",
|
|
20
|
+
"tritanopia",
|
|
21
|
+
"grayscale",
|
|
22
|
+
]);
|
|
23
|
+
const VISUAL_BOOLEANS = new Set([
|
|
24
|
+
"reduce_transparency",
|
|
25
|
+
"forced_colors",
|
|
26
|
+
"inverted_colors",
|
|
27
|
+
"uses_screen_reader",
|
|
28
|
+
"uses_magnifier",
|
|
29
|
+
"dyslexia_friendly_font",
|
|
30
|
+
]);
|
|
31
|
+
const VISUAL_STRINGS = {
|
|
32
|
+
text_size: TEXT_SIZE,
|
|
33
|
+
contrast_preference: CONTRAST_PREFERENCE,
|
|
34
|
+
color_scheme: COLOR_SCHEME,
|
|
35
|
+
color_filter: COLOR_FILTER,
|
|
36
|
+
};
|
|
37
|
+
const AUDITORY_BOOLEANS = new Set([
|
|
38
|
+
"captions_required",
|
|
39
|
+
"audio_descriptions_required",
|
|
40
|
+
"mono_audio",
|
|
41
|
+
"visual_alerts_required",
|
|
42
|
+
"uses_hearing_aid",
|
|
43
|
+
]);
|
|
44
|
+
const MOTOR_BOOLEANS = new Set([
|
|
45
|
+
"uses_switch_control",
|
|
46
|
+
"uses_voice_control",
|
|
47
|
+
"uses_eye_tracking",
|
|
48
|
+
"needs_larger_tap_targets",
|
|
49
|
+
"extended_interaction_timeouts",
|
|
50
|
+
"avoid_hover_interactions",
|
|
51
|
+
"sticky_keys",
|
|
52
|
+
]);
|
|
53
|
+
const COGNITIVE_BOOLEANS = new Set([
|
|
54
|
+
"reduce_motion",
|
|
55
|
+
"simple_language_preferred",
|
|
56
|
+
"extra_reading_time",
|
|
57
|
+
"avoid_flashing",
|
|
58
|
+
"predictable_navigation",
|
|
59
|
+
]);
|
|
60
|
+
const DATA_BOOLEANS = new Set(["reduce_data"]);
|
|
61
|
+
const TOP_LEVEL_KEYS = new Set([
|
|
62
|
+
"version",
|
|
63
|
+
"visual",
|
|
64
|
+
"auditory",
|
|
65
|
+
"motor",
|
|
66
|
+
"cognitive",
|
|
67
|
+
"data",
|
|
68
|
+
"assistive_tech",
|
|
69
|
+
"notes",
|
|
70
|
+
"extensions",
|
|
71
|
+
]);
|
|
72
|
+
function err(path, msg) {
|
|
73
|
+
return new Error(`Invalid --accessibility-profile at ${path}: ${msg}. See ${SPEC_URL}.`);
|
|
74
|
+
}
|
|
75
|
+
function checkGroup(group, path, bools, strs = {}) {
|
|
76
|
+
if (group === undefined || group === null)
|
|
77
|
+
return;
|
|
78
|
+
if (typeof group !== "object" || Array.isArray(group)) {
|
|
79
|
+
throw err(path, "must be an object");
|
|
80
|
+
}
|
|
81
|
+
for (const [key, value] of Object.entries(group)) {
|
|
82
|
+
if (bools.has(key)) {
|
|
83
|
+
if (typeof value !== "boolean")
|
|
84
|
+
throw err(`${path}.${key}`, "must be a boolean");
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (key in strs) {
|
|
88
|
+
const allowed = strs[key];
|
|
89
|
+
if (typeof value !== "string" || !allowed.has(value)) {
|
|
90
|
+
throw err(`${path}.${key}`, `must be one of ${[...allowed].join(", ")}`);
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
throw err(`${path}.${key}`, "unknown property");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function validateAccessibilityProfile(raw) {
|
|
98
|
+
if (raw === undefined || raw === null) {
|
|
99
|
+
throw err("$", "must be a JSON object");
|
|
100
|
+
}
|
|
101
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
102
|
+
throw err("$", "must be a JSON object");
|
|
103
|
+
}
|
|
104
|
+
const obj = raw;
|
|
105
|
+
const keys = Object.keys(obj);
|
|
106
|
+
if (keys.length === 0)
|
|
107
|
+
return {};
|
|
108
|
+
for (const key of keys) {
|
|
109
|
+
if (!TOP_LEVEL_KEYS.has(key)) {
|
|
110
|
+
throw err(`$.${key}`, "unknown property");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (obj.version !== "1.0") {
|
|
114
|
+
throw err("$.version", 'is required and must be "1.0" when the object is non-empty');
|
|
115
|
+
}
|
|
116
|
+
checkGroup(obj.visual, "$.visual", VISUAL_BOOLEANS, VISUAL_STRINGS);
|
|
117
|
+
checkGroup(obj.auditory, "$.auditory", AUDITORY_BOOLEANS);
|
|
118
|
+
checkGroup(obj.motor, "$.motor", MOTOR_BOOLEANS);
|
|
119
|
+
checkGroup(obj.cognitive, "$.cognitive", COGNITIVE_BOOLEANS);
|
|
120
|
+
checkGroup(obj.data, "$.data", DATA_BOOLEANS);
|
|
121
|
+
if (obj.assistive_tech !== undefined) {
|
|
122
|
+
if (!Array.isArray(obj.assistive_tech)
|
|
123
|
+
|| !obj.assistive_tech.every((v) => typeof v === "string")) {
|
|
124
|
+
throw err("$.assistive_tech", "must be an array of strings");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (obj.notes !== undefined && typeof obj.notes !== "string") {
|
|
128
|
+
throw err("$.notes", "must be a string");
|
|
129
|
+
}
|
|
130
|
+
if (obj.extensions !== undefined) {
|
|
131
|
+
if (typeof obj.extensions !== "object" || obj.extensions === null || Array.isArray(obj.extensions)) {
|
|
132
|
+
throw err("$.extensions", "must be an object");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return obj;
|
|
136
|
+
}
|
package/dist/lib/alias-store.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import { resolve as resolvePath } from "node:path";
|
|
10
|
+
import { normalizeEnumValue, QUESTION_TYPES } from "./enums.js";
|
|
10
11
|
export function loadQuestionsManifest(filePath) {
|
|
11
12
|
let raw;
|
|
12
13
|
try {
|
|
@@ -30,6 +31,14 @@ export function loadQuestionsManifest(filePath) {
|
|
|
30
31
|
if (!q || typeof q !== "object" || typeof q.question !== "string" || !q.question.trim()) {
|
|
31
32
|
throw new Error(`questions[${i}].question must be a non-empty string.`);
|
|
32
33
|
}
|
|
34
|
+
// Fold underscored variants (`single_choice`) back to the canonical
|
|
35
|
+
// hyphenated form (`single-choice`). Unknown types pass through untouched
|
|
36
|
+
// so the backend remains the source of truth for shape validation.
|
|
37
|
+
if (typeof q.type === "string") {
|
|
38
|
+
const canonical = normalizeEnumValue(q.type, QUESTION_TYPES);
|
|
39
|
+
if (canonical !== null)
|
|
40
|
+
q.type = canonical;
|
|
41
|
+
}
|
|
33
42
|
}
|
|
34
43
|
return parsed;
|
|
35
44
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit cost estimators — mirror the backend's billing formulas so the CLI
|
|
3
|
+
* can surface a pre-dispatch estimate without a network round-trip.
|
|
4
|
+
*
|
|
5
|
+
* The numbers here MUST match `ish-backend/app/{media,chat}/billing.py`
|
|
6
|
+
* and `app/billing/service.py`. If the backend formula changes, update
|
|
7
|
+
* this file in the same commit, otherwise the CLI will mislead agents.
|
|
8
|
+
*
|
|
9
|
+
* Today every modality uses the same shape: `max(1, round(steps / 10))`
|
|
10
|
+
* per principal (per tester for media/interactive, per conversation for
|
|
11
|
+
* chat, ×2 for tester-pair). Asks bill flat 1 credit per successful
|
|
12
|
+
* tester response. These are intentionally per-run estimates; long-term
|
|
13
|
+
* we'll fetch `GET /billing/rates` and parameterise modalities — see
|
|
14
|
+
* `reference/credits` docs page.
|
|
15
|
+
*/
|
|
16
|
+
export interface CreditEstimate {
|
|
17
|
+
/** Upper bound (no early termination). Never claims exactness. */
|
|
18
|
+
upper_bound: number;
|
|
19
|
+
/** Stable identifier so agents can branch on shape if the formula evolves. */
|
|
20
|
+
formula: "media_per_tester" | "chat_solo" | "chat_pair" | "ask_per_response";
|
|
21
|
+
/** Human-readable breakdown so agents can explain the number to users. */
|
|
22
|
+
breakdown: string;
|
|
23
|
+
/** Always "credits" today; reserved for future-proofing (e.g. millicredits). */
|
|
24
|
+
unit: "credits";
|
|
25
|
+
}
|
|
26
|
+
/** Mirror of `app/media/billing.py::media_credit_cost`. */
|
|
27
|
+
export declare function mediaCreditCost(steps: number): number;
|
|
28
|
+
/** Mirror of `app/chat/billing.py::chat_credit_cost`. */
|
|
29
|
+
export declare function chatCreditCost(turns: number): number;
|
|
30
|
+
/**
|
|
31
|
+
* Media/interactive run: 1 credit-cost-per-tester × tester count. Modality
|
|
32
|
+
* doesn't currently affect the rate (interactive == text == video at the
|
|
33
|
+
* billing layer) — kept as a parameter for forward compatibility.
|
|
34
|
+
*/
|
|
35
|
+
export declare function estimateMediaRun(args: {
|
|
36
|
+
testerCount: number;
|
|
37
|
+
maxInteractions: number;
|
|
38
|
+
}): CreditEstimate;
|
|
39
|
+
/** Solo chat (single tester, external chatbot). */
|
|
40
|
+
export declare function estimateChatSolo(args: {
|
|
41
|
+
testerCount: number;
|
|
42
|
+
maxTurns: number;
|
|
43
|
+
}): CreditEstimate;
|
|
44
|
+
/** Tester-pair chat: each turn bills both sides, so cost doubles. */
|
|
45
|
+
export declare function estimateChatPair(args: {
|
|
46
|
+
conversationCount: number;
|
|
47
|
+
maxTurns: number;
|
|
48
|
+
}): CreditEstimate;
|
|
49
|
+
/**
|
|
50
|
+
* Ask round: flat 1 credit per successful tester response (charged only
|
|
51
|
+
* for completed responses; the upper bound assumes everyone completes).
|
|
52
|
+
*/
|
|
53
|
+
export declare function estimateAskRound(args: {
|
|
54
|
+
testerCount: number;
|
|
55
|
+
}): CreditEstimate;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit cost estimators — mirror the backend's billing formulas so the CLI
|
|
3
|
+
* can surface a pre-dispatch estimate without a network round-trip.
|
|
4
|
+
*
|
|
5
|
+
* The numbers here MUST match `ish-backend/app/{media,chat}/billing.py`
|
|
6
|
+
* and `app/billing/service.py`. If the backend formula changes, update
|
|
7
|
+
* this file in the same commit, otherwise the CLI will mislead agents.
|
|
8
|
+
*
|
|
9
|
+
* Today every modality uses the same shape: `max(1, round(steps / 10))`
|
|
10
|
+
* per principal (per tester for media/interactive, per conversation for
|
|
11
|
+
* chat, ×2 for tester-pair). Asks bill flat 1 credit per successful
|
|
12
|
+
* tester response. These are intentionally per-run estimates; long-term
|
|
13
|
+
* we'll fetch `GET /billing/rates` and parameterise modalities — see
|
|
14
|
+
* `reference/credits` docs page.
|
|
15
|
+
*/
|
|
16
|
+
/** Mirror of `app/media/billing.py::media_credit_cost`. */
|
|
17
|
+
export function mediaCreditCost(steps) {
|
|
18
|
+
if (!Number.isFinite(steps) || steps <= 0)
|
|
19
|
+
return 1;
|
|
20
|
+
return Math.max(1, Math.round(steps / 10));
|
|
21
|
+
}
|
|
22
|
+
/** Mirror of `app/chat/billing.py::chat_credit_cost`. */
|
|
23
|
+
export function chatCreditCost(turns) {
|
|
24
|
+
if (!Number.isFinite(turns) || turns <= 0)
|
|
25
|
+
return 1;
|
|
26
|
+
return Math.max(1, Math.round(turns / 10));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Media/interactive run: 1 credit-cost-per-tester × tester count. Modality
|
|
30
|
+
* doesn't currently affect the rate (interactive == text == video at the
|
|
31
|
+
* billing layer) — kept as a parameter for forward compatibility.
|
|
32
|
+
*/
|
|
33
|
+
export function estimateMediaRun(args) {
|
|
34
|
+
const perTester = mediaCreditCost(args.maxInteractions);
|
|
35
|
+
const total = Math.max(0, args.testerCount) * perTester;
|
|
36
|
+
return {
|
|
37
|
+
upper_bound: total,
|
|
38
|
+
formula: "media_per_tester",
|
|
39
|
+
breakdown: `${args.testerCount} tester(s) × max(1, round(${args.maxInteractions} steps / 10)) = ${args.testerCount} × ${perTester} = ${total}`,
|
|
40
|
+
unit: "credits",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Solo chat (single tester, external chatbot). */
|
|
44
|
+
export function estimateChatSolo(args) {
|
|
45
|
+
const perTester = chatCreditCost(args.maxTurns);
|
|
46
|
+
const total = Math.max(0, args.testerCount) * perTester;
|
|
47
|
+
return {
|
|
48
|
+
upper_bound: total,
|
|
49
|
+
formula: "chat_solo",
|
|
50
|
+
breakdown: `${args.testerCount} tester(s) × max(1, round(${args.maxTurns} turns / 10)) = ${args.testerCount} × ${perTester} = ${total}`,
|
|
51
|
+
unit: "credits",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Tester-pair chat: each turn bills both sides, so cost doubles. */
|
|
55
|
+
export function estimateChatPair(args) {
|
|
56
|
+
const perSide = chatCreditCost(args.maxTurns);
|
|
57
|
+
const total = Math.max(0, args.conversationCount) * perSide * 2;
|
|
58
|
+
return {
|
|
59
|
+
upper_bound: total,
|
|
60
|
+
formula: "chat_pair",
|
|
61
|
+
breakdown: `${args.conversationCount} conv × max(1, round(${args.maxTurns} turns / 10)) × 2 sides = ${args.conversationCount} × ${perSide} × 2 = ${total}`,
|
|
62
|
+
unit: "credits",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Ask round: flat 1 credit per successful tester response (charged only
|
|
67
|
+
* for completed responses; the upper bound assumes everyone completes).
|
|
68
|
+
*/
|
|
69
|
+
export function estimateAskRound(args) {
|
|
70
|
+
const total = Math.max(0, args.testerCount);
|
|
71
|
+
return {
|
|
72
|
+
upper_bound: total,
|
|
73
|
+
formula: "ask_per_response",
|
|
74
|
+
breakdown: `${args.testerCount} tester(s) × 1 credit/response = ${total} (upper bound; only successful responses bill)`,
|
|
75
|
+
unit: "credits",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -129,6 +129,12 @@ export declare function resolveAsk(explicit?: string): string;
|
|
|
129
129
|
* endpoint persisted by `ish chat endpoint use`. Throws when none are set.
|
|
130
130
|
*/
|
|
131
131
|
export declare function resolveChatEndpoint(positional?: string, flag?: string): string;
|
|
132
|
+
/**
|
|
133
|
+
* Resolve a chat configuration id from a positional arg or `--config` flag.
|
|
134
|
+
* Configurations don't have an "active" notion (unlike endpoints), so this
|
|
135
|
+
* helper only resolves explicit input — there is no fallback.
|
|
136
|
+
*/
|
|
137
|
+
export declare function resolveChatConfig(positional?: string, flag?: string): string;
|
|
132
138
|
/** Commander option-collector for repeatable flags (e.g. `--variant text:"..."` repeated). */
|
|
133
139
|
export declare function collectRepeatable(value: string, prev?: string[]): string[];
|
|
134
140
|
/**
|
|
@@ -598,6 +598,18 @@ export function resolveChatEndpoint(positional, flag) {
|
|
|
598
598
|
return config.chat_endpoint;
|
|
599
599
|
throw new Error('No chat endpoint set. Use `ish chat endpoint use <id>`, pass the endpoint id, or set --endpoint.');
|
|
600
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* Resolve a chat configuration id from a positional arg or `--config` flag.
|
|
603
|
+
* Configurations don't have an "active" notion (unlike endpoints), so this
|
|
604
|
+
* helper only resolves explicit input — there is no fallback.
|
|
605
|
+
*/
|
|
606
|
+
export function resolveChatConfig(positional, flag) {
|
|
607
|
+
if (positional)
|
|
608
|
+
return resolveId(positional);
|
|
609
|
+
if (flag)
|
|
610
|
+
return resolveId(flag);
|
|
611
|
+
throw new Error("Pass a chat configuration alias / UUID (positional argument or --config <id>).");
|
|
612
|
+
}
|
|
601
613
|
/** Commander option-collector for repeatable flags (e.g. `--variant text:"..."` repeated). */
|
|
602
614
|
export function collectRepeatable(value, prev = []) {
|
|
603
615
|
return prev.concat([value]);
|