clay-server 5.2.0-beta.10 → 5.2.0-beta.11

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.
Files changed (98) hide show
  1. package/README.md +0 -4
  2. package/lib/clay-product-control-tools.js +1 -0
  3. package/lib/clay-product-control.js +14 -11
  4. package/lib/clay-vendor-usage.js +44 -0
  5. package/lib/github-star-count.js +147 -0
  6. package/lib/notes-checklist.js +43 -1
  7. package/lib/notes.js +2 -1
  8. package/lib/project-session-notes.js +33 -8
  9. package/lib/project-sessions.js +63 -24
  10. package/lib/project-start-protocol.js +29 -0
  11. package/lib/project-vendor-login.js +2 -0
  12. package/lib/project.js +3 -0
  13. package/lib/public/app.js +25 -17
  14. package/lib/public/css/contextual-clay.css +1 -1
  15. package/lib/public/css/filebrowser.css +1 -1
  16. package/lib/public/css/mate-loading-scene.css +21 -0
  17. package/lib/public/css/overlays.css +64 -256
  18. package/lib/public/css/project-mate-creation.css +5 -6
  19. package/lib/public/css/project-start.css +108 -0
  20. package/lib/public/css/projects-hub.css +53 -14
  21. package/lib/public/css/settings-shell.css +103 -0
  22. package/lib/public/css/sidebar.css +7 -0
  23. package/lib/public/css/title-bar.css +79 -0
  24. package/lib/public/index.html +50 -63
  25. package/lib/public/modules/admin-settings-invitations.js +2 -2
  26. package/lib/public/modules/admin-settings-projects.js +2 -2
  27. package/lib/public/modules/admin-settings-ui.js +20 -4
  28. package/lib/public/modules/admin-settings-users.js +4 -4
  29. package/lib/public/modules/app-home-hub.js +2 -1
  30. package/lib/public/modules/app-messages.js +10 -2
  31. package/lib/public/modules/app-projects.js +1 -407
  32. package/lib/public/modules/app-rate-limit.js +12 -1
  33. package/lib/public/modules/clay-tool-presentation.js +1 -0
  34. package/lib/public/modules/contextual-clay.js +3 -1
  35. package/lib/public/modules/github-star.js +66 -0
  36. package/lib/public/modules/mate-loading-scene.js +17 -0
  37. package/lib/public/modules/profile.js +11 -3
  38. package/lib/public/modules/project-mate-creation-workspace.js +13 -4
  39. package/lib/public/modules/project-session-visibility-settings.js +19 -16
  40. package/lib/public/modules/project-settings.js +119 -129
  41. package/lib/public/modules/project-start-dialog.js +130 -0
  42. package/lib/public/modules/project-start-folder-state.js +78 -0
  43. package/lib/public/modules/project-start-folder-view.js +190 -0
  44. package/lib/public/modules/project-start-forms.js +66 -0
  45. package/lib/public/modules/project-start-path.js +60 -0
  46. package/lib/public/modules/project-start-state.js +54 -0
  47. package/lib/public/modules/project-start-submit.js +67 -0
  48. package/lib/public/modules/projects-hub-view.js +33 -14
  49. package/lib/public/modules/projects-hub.js +10 -6
  50. package/lib/public/modules/server-settings.js +36 -91
  51. package/lib/public/modules/settings-project-target.js +56 -0
  52. package/lib/public/modules/settings-select.js +1 -1
  53. package/lib/public/modules/settings-shell-registry.js +88 -0
  54. package/lib/public/modules/settings-shell-view.js +104 -0
  55. package/lib/public/modules/settings-shell.js +342 -0
  56. package/lib/public/modules/sidebar-mobile.js +8 -30
  57. package/lib/public/modules/sidebar-projects.js +2 -1
  58. package/lib/public/modules/theme.js +10 -2
  59. package/lib/public/modules/user-settings-layout.js +8 -33
  60. package/lib/public/modules/user-settings-profile.js +1 -1
  61. package/lib/public/modules/user-settings.js +18 -85
  62. package/lib/public/modules/vendor-usage-format.js +93 -0
  63. package/lib/public/modules/vendor-usage-list.js +45 -0
  64. package/lib/public/modules/vendor-usage-model.js +215 -0
  65. package/lib/public/modules/vendor-usage-overview.js +312 -0
  66. package/lib/public/style.css +10 -7
  67. package/lib/public/welcome-art/conversation-desk.png +0 -0
  68. package/lib/public/welcome-art/mate-clay.png +0 -0
  69. package/lib/public/welcome-art/project-clay.png +0 -0
  70. package/lib/sdk-bridge.js +6 -1
  71. package/lib/sdk-message-processor.js +9 -1
  72. package/lib/sdk-quota-snapshot.js +20 -2
  73. package/lib/server-github-stars.js +39 -0
  74. package/lib/server-global-ws.js +21 -17
  75. package/lib/server.js +4 -0
  76. package/lib/session-notes-mcp-server.js +26 -4
  77. package/lib/session-notes-writes.js +88 -0
  78. package/lib/session-visibility.js +20 -8
  79. package/lib/sessions.js +2 -0
  80. package/lib/vendor-usage-acp-readers.js +97 -0
  81. package/lib/vendor-usage-capabilities.js +88 -0
  82. package/lib/vendor-usage-http-readers.js +214 -0
  83. package/lib/vendor-usage-metrics.js +47 -0
  84. package/lib/vendor-usage-native-readers.js +106 -0
  85. package/lib/vendor-usage-overview.js +406 -0
  86. package/lib/vendor-usage-service.js +281 -0
  87. package/lib/ws-schema.js +4 -2
  88. package/lib/yoke/adapters/claude.js +12 -0
  89. package/lib/yoke/adapters/codex-query-contract.js +1 -0
  90. package/lib/yoke/adapters/codex.js +45 -12
  91. package/lib/yoke/claude-hook-installer.js +1 -0
  92. package/lib/yoke/codex-dynamic-tool-names.js +80 -0
  93. package/lib/yoke/codex-model-catalog.js +37 -3
  94. package/lib/yoke/muse-services.js +16 -3
  95. package/lib/yoke/providers/claude-model-descriptions.js +22 -2
  96. package/lib/yoke/vendor-registry.js +6 -2
  97. package/package.json +2 -1
  98. package/lib/public/modules/server-settings-navigation.js +0 -39
package/lib/server.js CHANGED
@@ -507,6 +507,8 @@ function createServer(opts) {
507
507
  var linear = require('./server-linear').attachLinear({ users: users, getMultiUserFromReq: getMultiUserFromReq,
508
508
  isRequestAuthed: isRequestAuthed, CONFIG_DIR: CONFIG_DIR });
509
509
 
510
+ var githubStars = require('./server-github-stars').attachGithubStars({ isRequestAuthed: isRequestAuthed });
511
+
510
512
  var speech = serverSpeech.attachSpeech({ users: users, getMultiUserFromReq: getMultiUserFromReq,
511
513
  isRequestAuthed: isRequestAuthed, CONFIG_DIR: CONFIG_DIR, getAllowedOrigins: getAllowedOrigins });
512
514
 
@@ -925,6 +927,7 @@ function createServer(opts) {
925
927
 
926
928
  if (linear.handleRequest(req, res, fullUrl)) return;
927
929
  if (speech.handleRequest(req, res, fullUrl)) return;
930
+ if (githubStars.handleRequest(req, res, fullUrl)) return;
928
931
  if (networkSettings.handleRequest(req, res, fullUrl)) return;
929
932
  if (storageSettings.handleRequest(req, res, fullUrl)) return;
930
933
  if (settings.handleRequest(req, res, fullUrl)) return;
@@ -1590,6 +1593,7 @@ function createServer(opts) {
1590
1593
  return ctx.getSchedules().length;
1591
1594
  },
1592
1595
  canAccessProjectSlug: canUserAccessSlug,
1596
+ onGetProjectAccess: onGetProjectAccess,
1593
1597
  canUsePath: canUsePath,
1594
1598
  onPresenceChange: broadcastPresenceChange,
1595
1599
  onProcessingChanged: broadcastProcessingChange,
@@ -1,6 +1,8 @@
1
1
  // Sticky-note memory tools for project sessions.
2
2
 
3
3
  var buildShape = require("./session-spawn-mcp-server").buildShape;
4
+ var z;
5
+ try { z = require("zod"); } catch (e) { z = null; }
4
6
 
5
7
  var MEMORY_CONTRACT =
6
8
  "The sticky-note board persists across sessions and is a user-facing artifact shared with people and Clay agents, not private agent scratch space. " +
@@ -8,15 +10,28 @@ var MEMORY_CONTRACT =
8
10
  "Use Sticky Notes for preferences, reminders, undeveloped ideas, or an explicit request to remember something; confirmed Issues also get short link notes as described below. For concrete actionable bugs, improvements, or deferred implementation, an authorized Project Driver should use Project Issues. If you lack Issues authority, route the work through an eligible Project Driver. Check active notes first when a duplicate is plausible. " +
9
11
  require("./issues-filing-guidance").NOTE_GUIDANCE +
10
12
  "Never create a note merely because work is important, lengthy, spans agents or restarts, or might help another agent. Do not record completed work, implementation details, test results, investigation logs, transient blockers, conversation summaries, or announcements of your own activity. When uncertain, do not write. " +
13
+ "One exception: when the user explicitly asks for a working checklist, keep that one native checklist note and check its items off as they are done; never start a checklist the user did not ask for. " +
11
14
  "Updates are visible too: update only when durable state materially changes. " +
12
15
  "A note is an attention item, so finishing it means closing it, never erasing it: call close_note once the thing it is asking for is done, and the note leaves the active board while the record stays and can be reopened. Do not turn a note into a completion log instead of closing it. " +
13
16
  "Put a concise plain-text title on the first line, stay focused on one topic, and include only the context needed for future action.";
14
17
 
18
+ var CHECKLIST_USAGE =
19
+ " Native checklists: pass checklist: true to create one. The first line is the title and every later non-empty, non-heading line is an item; write items as '- [ ] task' and '- [x] task' for items already done. " +
20
+ "To check or uncheck items, call list_notes, then pass id, that note's current version and checks with the exact item keys it returned. " +
21
+ "Any change to a checklist note, switching checklist mode, or changing checks needs the current version; a stale version or unknown item is refused, so list again and retry. " +
22
+ "Text, saved checks and colour you do not change are kept, and switching mode never erases content. Change text and checks in separate calls. " +
23
+ "When rewriting a checklist's text, start from checklist.effectiveText so each line's - [x] marker carries its real completion; droppedSavedChecks lists saved checks whose exact line no longer exists.";
24
+
25
+ var CHECK_ENTRY = z ? z.object({
26
+ item: z.string().describe("Exact item key from list_notes checklist.items[].item."),
27
+ checked: z.boolean().describe("true marks the item done; false marks it not done."),
28
+ }) : null;
29
+
15
30
  function getToolDefs(handlers) {
16
31
  return [
17
32
  {
18
33
  name: "list_notes",
19
- description: MEMORY_CONTRACT + " List the notes before writing when you need to avoid duplicates or inspect full memory beyond the injected summary. Lists open notes by default; pass state to see closed ones.",
34
+ description: MEMORY_CONTRACT + " List the notes before writing when you need to avoid duplicates or inspect full memory beyond the injected summary. Lists open notes by default; pass state to see closed ones. Each note includes its version and checklistMode; checklist notes also include checklist.items with exact item keys, each item's effective checked value (a saved check overrides the Markdown marker), its saved value, and completed/total counts.",
20
35
  inputSchema: buildShape({
21
36
  state: { type: "string", enum: ["open", "closed", "all"], description: "Which notes to list. Defaults to open, the notes still asking for action." },
22
37
  }),
@@ -24,12 +39,16 @@ function getToolDefs(handlers) {
24
39
  },
25
40
  {
26
41
  name: "write_note",
27
- description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id. For notes other than confirmed Issue link notes, apply this test: would the user likely choose to keep this visible on their board next week? If the answer is unclear, do not call this tool. The 20000-character limit is an abuse guard, not a target.",
42
+ description: MEMORY_CONTRACT + " Create a new note, or update an existing note by id. For notes other than confirmed Issue link notes and a working checklist the user explicitly asked for, apply this test: would the user likely choose to keep this visible on their board next week? If the answer is unclear, do not call this tool. The 20000-character limit is an abuse guard, not a target." + CHECKLIST_USAGE,
28
43
  inputSchema: buildShape({
29
44
  id: { type: "string", description: "Existing note id to update. Omit to create a note." },
30
- text: { type: "string", description: "User-facing sticky-note text with a concise title on the first line and only durable, action-relevant context below it." },
45
+ text: { type: "string", description: "User-facing sticky-note text with a concise title on the first line and only durable, action-relevant context below it. Required when creating; omit on update to keep the current text." },
31
46
  color: { type: "string", enum: ["yellow", "blue", "green", "pink", "orange", "purple"], description: "Optional sticky-note color." },
32
- }, ["text"]),
47
+ checklist: { type: "boolean", description: "true creates or switches to a native checklist, false switches back to an ordinary note. Switching keeps the text and saved checks." },
48
+ version: { type: "number", description: "The note's current version from list_notes. Required for any change to a checklist note, a checklist mode switch, or checks." },
49
+ // buildShape returns no fields when zod is unavailable.
50
+ checks: { schema: CHECK_ENTRY && z.array(CHECK_ENTRY).min(1).max(1000), description: "Checklist items to check or uncheck on an existing checklist note, using exact item keys from list_notes." },
51
+ }),
33
52
  handler: function (args) { return handlers.write(args || {}); },
34
53
  },
35
54
  {
@@ -60,6 +79,9 @@ function getToolDefs(handlers) {
60
79
  }
61
80
 
62
81
  module.exports = {
82
+ // 2: native checklist fields. Conversation-scoped catalogs older than this
83
+ // receive the current schema through exact-query recovery.
84
+ CATALOG_VERSION: 2,
63
85
  MEMORY_CONTRACT: MEMORY_CONTRACT,
64
86
  getToolDefs: getToolDefs,
65
87
  };
@@ -0,0 +1,88 @@
1
+ // Pure planning for agent sticky-note updates, including native checklists.
2
+ // Every checklist-affecting write must name the note's current version and
3
+ // exact item keys, so a stale view can never overwrite newer content or marks.
4
+
5
+ var checklist = require("./notes-checklist");
6
+
7
+ var MAX_CHECKS = 1000;
8
+
9
+ function validateArgs(args) {
10
+ if (args.checklist !== undefined && typeof args.checklist !== "boolean") return "checklist must be true or false";
11
+ if (args.version !== undefined && !Number.isFinite(args.version)) return "version must be the number returned by list_notes";
12
+ if (args.checks === undefined) return null;
13
+ if (!Array.isArray(args.checks) || args.checks.length === 0 || args.checks.length > MAX_CHECKS) {
14
+ return "checks must be a non-empty array of at most " + MAX_CHECKS + " { item, checked } entries";
15
+ }
16
+ for (var i = 0; i < args.checks.length; i++) {
17
+ var entry = args.checks[i];
18
+ if (!entry || typeof entry !== "object" || typeof entry.item !== "string" || typeof entry.checked !== "boolean") {
19
+ return "each checks entry needs an item key from list_notes and checked true or false";
20
+ }
21
+ }
22
+ return null;
23
+ }
24
+
25
+ function staleError(note) {
26
+ return "note " + note.id + " has changed or the version is missing; call list_notes, review its current text and items, and retry with that version";
27
+ }
28
+
29
+ // Saved marks whose exact item line no longer exists after a text change.
30
+ function droppedChecks(note, nextText) {
31
+ var kept = checklist.normalizeState(note.checklistState, nextText);
32
+ return checklist.describe(note).filter(function (entry) {
33
+ return entry.saved !== null && kept[decodeURIComponent(entry.item)] === undefined;
34
+ }).map(function (entry) {
35
+ return { text: entry.text, checked: entry.saved };
36
+ });
37
+ }
38
+
39
+ function planUpdate(note, args, text, color) {
40
+ var isChecklist = note.checklistMode === true;
41
+ var modeChange = typeof args.checklist === "boolean" && args.checklist !== isChecklist;
42
+ var hasText = typeof args.text === "string";
43
+ var hasChecks = Array.isArray(args.checks);
44
+ var needsVersion = isChecklist || modeChange || hasChecks;
45
+ if ((needsVersion || args.version !== undefined) && args.version !== note.updatedAt) return { error: staleError(note) };
46
+ if (hasText && !text) return { error: "text must not be empty; close the note instead of erasing it" };
47
+ if (hasText && hasChecks) return { error: "change text and checks in separate calls so item keys refer to one exact version" };
48
+
49
+ var changes = {};
50
+ var dropped = [];
51
+ if (hasText && text !== note.text) {
52
+ changes.text = text;
53
+ dropped = droppedChecks(note, text);
54
+ if (isChecklist && !modeChange) {
55
+ // Storage repeats the same exact-source and version guard atomically.
56
+ changes.checklistEdit = true;
57
+ changes.checklistSource = note.text;
58
+ changes.checklistVersion = note.updatedAt;
59
+ }
60
+ }
61
+ if (color && color !== note.color) changes.color = color;
62
+ if (modeChange) changes.checklistMode = args.checklist;
63
+
64
+ if (hasChecks) {
65
+ var resultMode = modeChange ? args.checklist : isChecklist;
66
+ if (!resultMode) return { error: "checks apply only to checklist notes; pass checklist: true to switch this note to a checklist without changing its text" };
67
+ var state = checklist.normalizeState(note.checklistState, note.text);
68
+ var requested = Object.create(null);
69
+ for (var i = 0; i < args.checks.length; i++) {
70
+ var found = checklist.findItem(note, args.checks[i].item);
71
+ if (!found) return { error: "checklist item not found in note " + note.id + "; call list_notes and use its current item keys" };
72
+ if (requested[found.key] !== undefined && requested[found.key] !== args.checks[i].checked) {
73
+ return { error: "checks names the same item more than once with different values" };
74
+ }
75
+ requested[found.key] = args.checks[i].checked;
76
+ state[found.key] = args.checks[i].checked;
77
+ }
78
+ changes.checklistState = state;
79
+ }
80
+
81
+ if (!hasText && !hasChecks && !color && args.checklist === undefined) {
82
+ return { error: "nothing to change; pass text, color, checklist, or checks" };
83
+ }
84
+ // An identical request is acknowledged without bumping the version.
85
+ return { changes: Object.keys(changes).length ? changes : null, droppedSavedChecks: dropped };
86
+ }
87
+
88
+ module.exports = { validateArgs: validateArgs, planUpdate: planUpdate, MAX_CHECKS: MAX_CHECKS };
@@ -36,16 +36,29 @@ function handleDefaultChange(ctx, ws, msg) {
36
36
  defaultChangeResult(ctx, ws, msg || {}, { ok: false, error: "Session visibility default must be private or shared." });
37
37
  return true;
38
38
  }
39
- if (msg.slug && msg.slug !== ctx.slug) {
40
- defaultChangeResult(ctx, ws, msg, { ok: false, error: "Project settings access is not permitted" });
41
- return true;
39
+ var denied = { ok: false, error: "Project settings access is not permitted" };
40
+ // The target is named by the client, so it is authorised here, never trusted:
41
+ // another project must be accessible to this account and its own access
42
+ // record (owner, worktree, Mate) is the one checked below.
43
+ var target = msg.slug && typeof msg.slug === "string" ? msg.slug : ctx.slug;
44
+ var access;
45
+ if (target === ctx.slug) {
46
+ if (ctx.isMate === true) { defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" }); return true; }
47
+ access = ctx.getProjectAccess ? ctx.getProjectAccess() : null;
48
+ } else {
49
+ if (/^mate-/.test(target) || typeof ctx.getTargetAccess !== "function" ||
50
+ typeof ctx.canAccessProjectSlug !== "function" || ctx.canAccessProjectSlug(ws, target) !== true) {
51
+ defaultChangeResult(ctx, ws, msg, denied);
52
+ return true;
53
+ }
54
+ access = ctx.getTargetAccess(target);
55
+ if (!access || access.error || access.isMate === true) { defaultChangeResult(ctx, ws, msg, denied); return true; }
42
56
  }
43
- if (ctx.isMate === true || !ctx.usersModule || !ctx.usersModule.isMultiUser || !ctx.usersModule.isMultiUser()) {
57
+ if (!ctx.usersModule || !ctx.usersModule.isMultiUser || !ctx.usersModule.isMultiUser()) {
44
58
  defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" });
45
59
  return true;
46
60
  }
47
61
  var user = ws && ws._clayUser && ctx.usersModule.findUserById(ws._clayUser.id);
48
- var access = ctx.getProjectAccess ? ctx.getProjectAccess() : null;
49
62
  if (access && access.isWorktree === true) {
50
63
  defaultChangeResult(ctx, ws, msg, { ok: false, error: "Worktrees inherit this setting from their parent project." });
51
64
  return true;
@@ -53,16 +66,15 @@ function handleDefaultChange(ctx, ws, msg) {
53
66
  var permissions = user && ctx.usersModule.getEffectivePermissions
54
67
  ? ctx.usersModule.getEffectivePermissions(user, ctx.osUsers) : {};
55
68
  var isOwner = !!(user && access && access.ownerId && access.ownerId === user.id);
56
- var isAdmin = !!(user && user.role === "admin");
57
69
  if (!user || !permissions.projectSettings || !isOwner) {
58
- defaultChangeResult(ctx, ws, msg, { ok: false, error: "Project settings access is not permitted" });
70
+ defaultChangeResult(ctx, ws, msg, denied);
59
71
  return true;
60
72
  }
61
73
  if (typeof ctx.opts.onSetProjectSessionVisibilityDefault !== "function") {
62
74
  defaultChangeResult(ctx, ws, msg, { ok: false, error: "Not supported" });
63
75
  return true;
64
76
  }
65
- var result = ctx.opts.onSetProjectSessionVisibilityDefault(ctx.slug, msg.visibility);
77
+ var result = ctx.opts.onSetProjectSessionVisibilityDefault(target, msg.visibility);
66
78
  defaultChangeResult(ctx, ws, msg, { ok: !!(result && result.ok), visibility: result && result.visibility, error: result && result.error });
67
79
  return true;
68
80
  }
package/lib/sessions.js CHANGED
@@ -166,6 +166,7 @@ function createSessionManager(opts) {
166
166
  if (session.codexLinearToolCatalogVersion) metaObj.codexLinearToolCatalogVersion = session.codexLinearToolCatalogVersion;
167
167
  if (session.codexSharedBrowserToolCatalogVersion) metaObj.codexSharedBrowserToolCatalogVersion = session.codexSharedBrowserToolCatalogVersion;
168
168
  if (session.codexIssuesToolCatalogVersion) metaObj.codexIssuesToolCatalogVersion = session.codexIssuesToolCatalogVersion;
169
+ if (session.codexNotesToolCatalogVersion) metaObj.codexNotesToolCatalogVersion = session.codexNotesToolCatalogVersion;
169
170
  if (session.model) metaObj.model = session.model;
170
171
  if (session.effort) metaObj.effort = session.effort;
171
172
  // Persist the session's "born" mode so TUI sessions reappear after a
@@ -358,6 +359,7 @@ function createSessionManager(opts) {
358
359
  session.codexLinearToolCatalogVersion = Number(m.codexLinearToolCatalogVersion || 0);
359
360
  session.codexSharedBrowserToolCatalogVersion = Number(m.codexSharedBrowserToolCatalogVersion || 0);
360
361
  session.codexIssuesToolCatalogVersion = Number(m.codexIssuesToolCatalogVersion || 0);
362
+ session.codexNotesToolCatalogVersion = Number(m.codexNotesToolCatalogVersion || 0);
361
363
  session.model = m.model || deriveSessionModel(loaded[i].history);
362
364
  session.effort = m.effort || null;
363
365
  if (m.loop) session.loop = m.loop;
@@ -0,0 +1,97 @@
1
+ // Kiro account usage through Kiro's own ACP server.
2
+ //
3
+ // Source (verified against the installed kiro-cli 2.32.0, not a web snippet):
4
+ // `kiro-cli acp` advertises `/usage` ("Show billing and usage information") in
5
+ // its available commands, and `_kiro.dev/commands/execute` with command "usage"
6
+ // answers with { success, message, data: { planName, billingCycleReset,
7
+ // overagesEnabled, usageBreakdowns: [{ resourceType, displayName, used, limit,
8
+ // percentage, hasLimit, currency, overageCharges, ... }] } }. The probe made no
9
+ // model turn. It does create an empty local Kiro session record (session/new is
10
+ // required before a command can run), under a neutral temp directory.
11
+ //
12
+ // A short-lived process is started per read, as the server user, with a hard
13
+ // timeout and kill; nothing is retained. Because the credential belongs to the
14
+ // server user, the capability table restricts it to single-user mode.
15
+
16
+ var m = require("./vendor-usage-metrics");
17
+ var failure = require("./vendor-usage-native-readers").failure;
18
+
19
+ function rpc(child, onFrame) {
20
+ var next = 0;
21
+ var pending = Object.create(null);
22
+ var buffer = "";
23
+ child.stdout.on("data", function (chunk) {
24
+ buffer += chunk;
25
+ if (buffer.length > 4 * 1024 * 1024) { buffer = ""; return; }
26
+ var lines = buffer.split("\n");
27
+ buffer = lines.pop();
28
+ lines.forEach(function (line) {
29
+ var frame;
30
+ try { frame = JSON.parse(line); } catch (e) { return; }
31
+ if (frame && frame.id !== undefined && pending[frame.id]) {
32
+ var entry = pending[frame.id];
33
+ delete pending[frame.id];
34
+ if (frame.error) entry.reject(failure("server", "Kiro rejected the usage request."));
35
+ else entry.resolve(frame.result);
36
+ } else if (onFrame) onFrame(frame);
37
+ });
38
+ });
39
+ return {
40
+ request: function (method, params) {
41
+ var id = ++next;
42
+ return new Promise(function (resolve, reject) {
43
+ pending[id] = { resolve: resolve, reject: reject };
44
+ child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: id, method: method, params: params }) + "\n");
45
+ });
46
+ },
47
+ failAll: function (error) { Object.keys(pending).forEach(function (id) { pending[id].reject(error); delete pending[id]; }); },
48
+ };
49
+ }
50
+
51
+ function mapUsage(data) {
52
+ var metrics = [];
53
+ var reset = typeof data.billingCycleReset === "string" ? Date.parse(data.billingCycleReset) : NaN;
54
+ var resetsAtMs = Number.isFinite(reset) ? reset : null;
55
+ (Array.isArray(data.usageBreakdowns) ? data.usageBreakdowns : []).forEach(function (item, index) {
56
+ if (!item || item.hasLimit !== true) return;
57
+ var used = m.num(item.used);
58
+ var limit = m.num(item.limit);
59
+ if (used === null || limit === null || limit <= 0) return;
60
+ var label = typeof item.displayName === "string" && item.displayName ? item.displayName : "Credits";
61
+ metrics.push(m.metric("credits", "kiro:" + (item.resourceType || index), label, { used: used, limit: limit, balance: null, unlimited: false, resetsAtMs: resetsAtMs }));
62
+ var charges = m.num(item.overageCharges);
63
+ if (data.overagesEnabled === true && charges !== null && typeof item.currency === "string" && item.currency) {
64
+ metrics.push(m.metric("currency", "kiro:overage:" + (item.resourceType || index), "Overage charges", { used: charges, limit: null, balance: null, currencyCode: item.currency, resetsAtMs: resetsAtMs }));
65
+ }
66
+ });
67
+ return { metrics: metrics, planType: typeof data.planName === "string" ? data.planName : null };
68
+ }
69
+
70
+ // deps: { binary(): path|null, spawn(command, args, options), tmpdir(), signal }
71
+ async function kiro(context, deps) {
72
+ var binary = deps.binary();
73
+ if (!binary) throw failure("unavailable", "The Kiro CLI was not found.");
74
+ var child = deps.spawn(binary, ["acp"], { stdio: ["pipe", "pipe", "ignore"], cwd: deps.tmpdir(), env: deps.env });
75
+ var link = rpc(child);
76
+ var died = new Promise(function (resolve, reject) {
77
+ child.on("error", function () { reject(failure("unavailable", "The Kiro CLI could not be started.")); });
78
+ child.on("exit", function () { reject(failure("unavailable", "The Kiro CLI exited before answering.")); });
79
+ });
80
+ died.catch(function () {});
81
+ var abort = context && context.signal;
82
+ var stop = function () { try { child.kill("SIGKILL"); } catch (e) {} link.failAll(failure("timeout", "The usage read was cancelled.")); };
83
+ if (abort) { if (abort.aborted) stop(); else abort.addEventListener("abort", stop, { once: true }); }
84
+ try {
85
+ await Promise.race([link.request("initialize", { protocolVersion: 1, clientCapabilities: {} }), died]);
86
+ var session = await Promise.race([link.request("session/new", { cwd: deps.tmpdir(), mcpServers: [] }), died]);
87
+ if (!session || typeof session.sessionId !== "string") throw failure("parse", "Kiro returned no session.");
88
+ var reply = await Promise.race([link.request("_kiro.dev/commands/execute", { sessionId: session.sessionId, command: { command: "usage", args: {} } }), died]);
89
+ if (!reply || reply.success !== true || !reply.data || typeof reply.data !== "object") throw failure("unavailable", "Kiro did not report usage for this account.");
90
+ return mapUsage(reply.data);
91
+ } finally {
92
+ if (abort) abort.removeEventListener("abort", stop);
93
+ try { child.kill("SIGKILL"); } catch (e) {}
94
+ }
95
+ }
96
+
97
+ module.exports = { kiro: kiro, mapUsage: mapUsage };
@@ -0,0 +1,88 @@
1
+ // Per-vendor usage capability for all eleven Clay vendors.
2
+ //
3
+ // mode:
4
+ // query Clay asks the vendor's own native source for account figures
5
+ // (a reader exists in lib/vendor-usage-*-readers.js).
6
+ // event The vendor pushes quota into a session; only the latest
7
+ // observed snapshot of the owner's own sessions is shown.
8
+ // unintegrated A source may exist, but it is not verified safe or not
9
+ // reachable from Clay's integration, so Clay does not read it.
10
+ // This is "not integrated", never "the provider has none".
11
+ //
12
+ // `observed` is true when session-observed quota can also be displayed (the
13
+ // existing rate-limit snapshot path). `units` lists the metric kinds the reader
14
+ // returns; they are never converted to each other. `identity` states what a
15
+ // direct account read needs: "owner-os-user" (the runtime runs as the owner's
16
+ // mapped OS user), "owner-session" (answered by one of the owner's own live
17
+ // sessions), "single-user" (the credential is the server user's; refused in
18
+ // account mode because no owner can be proven), or null.
19
+ //
20
+ // Evidence kinds in `source`/`reason`: "installed" = observed on the installed
21
+ // CLI/SDK in this repository's environment; "primary doc" = the vendor's own
22
+ // documentation; "Orca" = Orca@17a2bced code only (not independently run here).
23
+
24
+ var CAPABILITIES = {
25
+ claude: {
26
+ mode: "query", observed: true, units: ["percent", "credits", "currency"], identity: "owner-session",
27
+ source: "Claude SDK account usage call on one of your live sessions (installed SDK 0.3.x; the SDK marks it experimental, so it is capability-guarded and shape-checked); session rate_limit_event otherwise",
28
+ },
29
+ codex: {
30
+ mode: "query", observed: true, units: ["percent", "credits"], identity: "owner-os-user",
31
+ source: "codex app-server account/rateLimits/read (installed protocol schema) and account/rateLimits/updated events",
32
+ },
33
+ muse: {
34
+ mode: "query", observed: true, units: ["percent"], identity: "single-user",
35
+ source: "Muse Code usage/read (installed adapter) and usage/changed events",
36
+ },
37
+ antigravity: {
38
+ mode: "unintegrated", observed: false, units: [], identity: null,
39
+ reason: "Not integrated: the only known read is print-mode `agy -p /usage` (Orca), which Orca documents as spending a model turn on versions that answer it as a prompt. Stays disabled until a non-billable path is proven.",
40
+ },
41
+ opencode: {
42
+ mode: "unintegrated", observed: false, units: [], identity: null,
43
+ reason: "Not integrated: OpenCode Go quota exists (Orca reads it with an OpenCode Go key) but OpenCode hosts many backends and Clay cannot tell which one a session uses; ACP exposes context tokens only.",
44
+ },
45
+ kimi: {
46
+ mode: "query", observed: false, units: ["count"], identity: "single-user",
47
+ source: "Kimi Code GET /usages with the CLI's saved token (Orca; Kimi CLI not installed here). Counts only: the response does not name a period or unit.",
48
+ },
49
+ grok: {
50
+ mode: "query", observed: false, units: ["percent", "amount"], identity: "single-user",
51
+ source: "Grok CLI chat proxy GET /billing with the CLI's saved token (Orca; Grok CLI not installed here). Money fields carry no currency, so they stay unit-less amounts.",
52
+ },
53
+ copilot: {
54
+ mode: "unintegrated", observed: false, units: [], identity: null,
55
+ reason: "Not integrated: GitHub's Copilot SDK documents account.getQuota (entitlement, used requests, remaining %, reset date), but Clay talks to the Copilot CLI over ACP and ships no SDK client; ACP exposure of it is unverified.",
56
+ },
57
+ qwen: {
58
+ mode: "unintegrated", observed: false, units: [], identity: null,
59
+ reason: "Not integrated: Qwen Code's documented /stats and /context report session token and call metrics; no account quota read is documented (primary doc, checked this task).",
60
+ },
61
+ junie: {
62
+ mode: "unintegrated", observed: false, units: [], identity: null,
63
+ reason: "Not integrated: Junie CLI documents a /usage slash command (session cost and remaining balance), but running it through ACP could be treated as a prompt; side effects are unverified.",
64
+ },
65
+ kiro: {
66
+ mode: "query", observed: false, units: ["credits", "currency"], identity: "single-user",
67
+ source: "Kiro ACP `_kiro.dev/commands/execute` command usage (installed kiro-cli 2.32.0: structured credits used/limit and billing-cycle reset; no model turn observed). Creates an empty local session record.",
68
+ },
69
+ };
70
+
71
+ var VENDORS = Object.keys(CAPABILITIES);
72
+
73
+ function capabilityFor(vendor) {
74
+ var entry = CAPABILITIES[vendor];
75
+ if (!entry) return { mode: "unintegrated", observed: false, units: [], identity: null, reason: "Not integrated: no usage source has been reviewed for this vendor." };
76
+ return entry;
77
+ }
78
+
79
+ // Serializable form for the client; never includes functions.
80
+ function publicCapability(vendor) {
81
+ var entry = capabilityFor(vendor);
82
+ var result = { mode: entry.mode, observed: entry.observed === true, units: entry.units.slice() };
83
+ if (entry.source) result.source = entry.source;
84
+ if (entry.reason) result.reason = entry.reason;
85
+ return result;
86
+ }
87
+
88
+ module.exports = { VENDORS: VENDORS, capabilityFor: capabilityFor, publicCapability: publicCapability };