carmoji 0.3.7 → 0.3.8
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/carmoji.js +81 -3
- package/package.json +1 -1
package/carmoji.js
CHANGED
|
@@ -245,6 +245,7 @@ async function requestApproval(config, message, timeoutMs = 55_000, requireCap)
|
|
|
245
245
|
const reply = JSON.parse(data.toString());
|
|
246
246
|
if (reply.ok === false) return finish(null);
|
|
247
247
|
if (reply.decision === 'allow' || reply.decision === 'deny'
|
|
248
|
+
|| reply.decision === 'always'
|
|
248
249
|
|| typeof reply.answer === 'string') {
|
|
249
250
|
return finish(reply);
|
|
250
251
|
}
|
|
@@ -487,10 +488,41 @@ function normalizePayload(payload, eventFlag) {
|
|
|
487
488
|
return payload;
|
|
488
489
|
}
|
|
489
490
|
|
|
491
|
+
/** Tools that carry the agent's own task list in their input. */
|
|
492
|
+
const TODO_TOOLS = new Set(['TodoWrite', 'update_plan']);
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* The agent's task list, normalized from Claude's TodoWrite
|
|
496
|
+
* (`todos: [{content, status, activeForm}]`) or Codex's update_plan
|
|
497
|
+
* (`plan: [{step, status}]`). Every call carries the whole list, so the
|
|
498
|
+
* phone can render progress and diff for freshly completed items.
|
|
499
|
+
*/
|
|
500
|
+
function todosFromInput(input) {
|
|
501
|
+
const raw = Array.isArray(input?.todos) ? input.todos
|
|
502
|
+
: Array.isArray(input?.plan) ? input.plan : undefined;
|
|
503
|
+
if (!raw) return undefined;
|
|
504
|
+
const normalize = (status) =>
|
|
505
|
+
['completed', 'complete', 'done'].includes(status) ? 'done'
|
|
506
|
+
: ['in_progress', 'active'].includes(status) ? 'active' : 'pending';
|
|
507
|
+
const items = raw.slice(0, 12).map((item) => ({
|
|
508
|
+
text: String(item.content ?? item.step ?? item.text ?? '')
|
|
509
|
+
.replace(/\s+/g, ' ').trim().slice(0, 80),
|
|
510
|
+
status: normalize(item.status),
|
|
511
|
+
})).filter((item) => item.text);
|
|
512
|
+
return items.length ? items : undefined;
|
|
513
|
+
}
|
|
514
|
+
|
|
490
515
|
/** Map a normalized hook payload to a wire event, or null to ignore. */
|
|
491
516
|
function eventFromHook(payload) {
|
|
492
517
|
const raw = payload.hook_event_name;
|
|
493
518
|
const tool = payload.tool_name ?? EVENT_TOOL_HINTS[raw];
|
|
519
|
+
// The task list rides its own event: updating it is bookkeeping, not
|
|
520
|
+
// "running a command", so it must not pull the nervous tool face.
|
|
521
|
+
// Pre and Post both carry the list; sending twice is a no-op diff.
|
|
522
|
+
if (TODO_TOOLS.has(tool)) {
|
|
523
|
+
const todos = todosFromInput(payload.tool_input);
|
|
524
|
+
if (todos) return { event: 'todos', todos };
|
|
525
|
+
}
|
|
494
526
|
switch (EVENT_ALIASES[raw] ?? raw) {
|
|
495
527
|
case 'SessionStart': return { event: 'session_start' };
|
|
496
528
|
case 'UserPromptSubmit': return { event: 'prompt' };
|
|
@@ -617,6 +649,12 @@ const PERMISSION_HOOK_TIMEOUT_S = 150;
|
|
|
617
649
|
* `hookSpecificOutput.decision`. */
|
|
618
650
|
const ANSWERABLE_APPROVAL_SOURCES = new Set(['claude', 'codex']);
|
|
619
651
|
|
|
652
|
+
/** Sources whose decision also honors `updatedPermissions` — lets the phone
|
|
653
|
+
* offer "Always allow" (a session-scoped rule, mirroring the terminal
|
|
654
|
+
* dialog's don't-ask-again row). Codex hooks ignore updatedPermissions, so
|
|
655
|
+
* its card stays Allow/Deny. */
|
|
656
|
+
const ALWAYS_ALLOW_SOURCES = new Set(['claude']);
|
|
657
|
+
|
|
620
658
|
/** Sources whose question protocol accepts `decision.updatedInput`.
|
|
621
659
|
* Codex questions use App Server requestUserInput instead of hooks, so a
|
|
622
660
|
* synthetic AskUserQuestion event must remain display-only. */
|
|
@@ -1298,17 +1336,34 @@ async function main() {
|
|
|
1298
1336
|
// pleading face + toast; the picker stays in the terminal.
|
|
1299
1337
|
} else if (payload.tool_name !== 'AskUserQuestion'
|
|
1300
1338
|
&& ANSWERABLE_APPROVAL_SOURCES.has(source)) {
|
|
1339
|
+
const canAlways = ALWAYS_ALLOW_SOURCES.has(source)
|
|
1340
|
+
&& Boolean(payload.tool_name);
|
|
1301
1341
|
const decision = (await requestApprovalAll(devices, {
|
|
1302
1342
|
...common,
|
|
1303
1343
|
event: 'approval_request',
|
|
1304
1344
|
tool: payload.tool_name,
|
|
1305
1345
|
detail: summarizeToolInput(payload),
|
|
1346
|
+
// Tells the phone it may offer an "Always allow" button;
|
|
1347
|
+
// older app builds don't know the field and show Allow/Deny.
|
|
1348
|
+
...(canAlways ? { always: true } : {}),
|
|
1306
1349
|
}, APPROVAL_WAIT_MS))?.decision;
|
|
1307
1350
|
if (decision) {
|
|
1351
|
+
const settled = { behavior: decision === 'always' ? 'allow' : decision };
|
|
1352
|
+
if (decision === 'always' && canAlways) {
|
|
1353
|
+
// Session-scoped don't-ask-again rule. A bare toolName rule
|
|
1354
|
+
// allows every use of the tool; MCP tools (mcp__server__tool)
|
|
1355
|
+
// in particular reject a ruleContent specifier.
|
|
1356
|
+
settled.updatedPermissions = [{
|
|
1357
|
+
type: 'addRules',
|
|
1358
|
+
rules: [{ toolName: payload.tool_name }],
|
|
1359
|
+
behavior: 'allow',
|
|
1360
|
+
destination: 'session',
|
|
1361
|
+
}];
|
|
1362
|
+
}
|
|
1308
1363
|
console.log(JSON.stringify({
|
|
1309
1364
|
hookSpecificOutput: {
|
|
1310
1365
|
hookEventName: 'PermissionRequest',
|
|
1311
|
-
decision:
|
|
1366
|
+
decision: settled,
|
|
1312
1367
|
},
|
|
1313
1368
|
}));
|
|
1314
1369
|
}
|
|
@@ -1372,8 +1427,8 @@ async function main() {
|
|
|
1372
1427
|
|
|
1373
1428
|
case 'test': {
|
|
1374
1429
|
const event = TEST_EVENTS[arg];
|
|
1375
|
-
if (!event &&
|
|
1376
|
-
console.error(`Usage: carmoji test <${Object.keys(TEST_EVENTS).join('|')}|approval|question> [session] [tokens] [project] — details: carmoji test --help`);
|
|
1430
|
+
if (!event && !['approval', 'question', 'todos'].includes(arg)) {
|
|
1431
|
+
console.error(`Usage: carmoji test <${Object.keys(TEST_EVENTS).join('|')}|approval|question|todos> [session] [tokens] [project] — details: carmoji test --help`);
|
|
1377
1432
|
process.exit(1);
|
|
1378
1433
|
}
|
|
1379
1434
|
const devices = loadDevices();
|
|
@@ -1391,6 +1446,7 @@ async function main() {
|
|
|
1391
1446
|
event: 'approval_request',
|
|
1392
1447
|
tool: 'Bash',
|
|
1393
1448
|
detail: 'carmoji test approval — tap Allow or Deny',
|
|
1449
|
+
always: true,
|
|
1394
1450
|
}, APPROVAL_WAIT_MS))?.decision;
|
|
1395
1451
|
console.log(decision
|
|
1396
1452
|
? `Decision: ${decision}`
|
|
@@ -1422,6 +1478,28 @@ async function main() {
|
|
|
1422
1478
|
: 'No answer — in a real session the picker would stay in the terminal.');
|
|
1423
1479
|
process.exit(answer ? 0 : 1);
|
|
1424
1480
|
}
|
|
1481
|
+
if (arg === 'todos') {
|
|
1482
|
+
// Seeds a five-step demo list; raise `done` call by call to walk a
|
|
1483
|
+
// task across the board and fire the completed-item flourish:
|
|
1484
|
+
// carmoji test todos [session] [done 0-5]
|
|
1485
|
+
const [session, doneArg] = rest;
|
|
1486
|
+
const done = Math.max(0, Math.min(5, Number(doneArg ?? 1) || 0));
|
|
1487
|
+
const steps = ['Explore the codebase', 'Write the failing test',
|
|
1488
|
+
'Implement the fix', 'Run the test suite',
|
|
1489
|
+
'Update the docs'];
|
|
1490
|
+
const todos = steps.map((text, index) => ({
|
|
1491
|
+
text,
|
|
1492
|
+
status: index < done ? 'done' : index === done ? 'active' : 'pending',
|
|
1493
|
+
}));
|
|
1494
|
+
const message = { source: 'test', session, host: hostname(),
|
|
1495
|
+
event: 'todos', todos };
|
|
1496
|
+
const results = await sendAll(devices, message);
|
|
1497
|
+
results.forEach((result, index) => {
|
|
1498
|
+
console.log(` ${deviceLabel(devices[index])}: ${
|
|
1499
|
+
result === 'ok' ? 'sent!' : `failed (${result})`}`);
|
|
1500
|
+
});
|
|
1501
|
+
process.exit(results.every((result) => result === 'ok') ? 0 : 1);
|
|
1502
|
+
}
|
|
1425
1503
|
const [session, tokens, project] = rest;
|
|
1426
1504
|
const message = { source: 'test', session, host: hostname(), ...event };
|
|
1427
1505
|
if (tokens !== undefined) message.tokens = Number(tokens);
|