@mulmoclaude/core 5.4.0 → 5.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/helps/custom-view-remote.md +19 -10
- package/assets/helps/custom-view.md +2 -2
- package/assets/helps/error-recovery.md +39 -0
- package/dist/google/collectionPush.d.ts +29 -0
- package/dist/google/index.cjs +24 -5
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.d.ts +1 -1
- package/dist/google/index.js +24 -6
- package/dist/google/index.js.map +1 -1
- package/package.json +7 -7
|
@@ -60,7 +60,7 @@ window.__MC_VIEW = {
|
|
|
60
60
|
getItems: (opts) => Promise, // the ONLY way to read records — see below
|
|
61
61
|
updateItem: (id, patch) => Promise, // patch declared fields (see Writing records)
|
|
62
62
|
deleteItem: (id) => Promise, // remove a record (requires allowDelete)
|
|
63
|
-
startChat: (prompt, role) => void, // start a new chat
|
|
63
|
+
startChat: (prompt, role) => void, // start a new chat — draft, or sent when the view declares allowSendChat
|
|
64
64
|
t: (key, named) => string, // vue-i18n-style dict lookup (same as desktop)
|
|
65
65
|
};
|
|
66
66
|
```
|
|
@@ -220,16 +220,25 @@ only way a remote view "does" anything beyond display:
|
|
|
220
220
|
window.__MC_VIEW.startChat("Mark task " + item.id + " as done.");
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
**
|
|
224
|
-
|
|
225
|
-
would simply strand the work. (The desktop custom view drafts by default and
|
|
226
|
-
sends only when its `views[]` entry declares `allowSendChat: true`; the desktop
|
|
227
|
-
phone-frame preview follows that same declaration, so declare it there too if
|
|
228
|
-
you want the preview to behave the way the phone will.)
|
|
223
|
+
**The phone follows the view's `allowSendChat` declaration**, exactly as the
|
|
224
|
+
desktop does — it is read from the `views[]` entry, never from your code:
|
|
229
225
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
- **Not declared** — the prompt opens as an **editable draft** in the chat sheet,
|
|
227
|
+
and the user presses Send.
|
|
228
|
+
- **`"allowSendChat": true`** — **one press and the turn runs**.
|
|
229
|
+
|
|
230
|
+
```jsonc
|
|
231
|
+
{ "id": "tasks", "label": "Tasks", "file": "views/tasks.html", "target": "mobile", "allowSendChat": true }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Declare it whenever a button is meant to start work: an undeclared view makes
|
|
235
|
+
the user open the sheet and send by hand on every press. The desktop
|
|
236
|
+
phone-frame preview reads the same declaration, so it behaves the way the phone
|
|
237
|
+
will.
|
|
238
|
+
|
|
239
|
+
With it declared, whatever you compose becomes an agent turn verbatim, with
|
|
240
|
+
every tool available. Build the prompt from your own records, and don't paste
|
|
241
|
+
text you fetched from somewhere else into it.
|
|
233
242
|
|
|
234
243
|
### Translations — `t`
|
|
235
244
|
|
|
@@ -442,8 +442,8 @@ to reword.
|
|
|
442
442
|
- Whatever you compose becomes an agent turn verbatim, with every tool
|
|
443
443
|
available. Build the prompt from your own records, and don't paste text you
|
|
444
444
|
fetched from somewhere else into it.
|
|
445
|
-
- **
|
|
446
|
-
|
|
445
|
+
- **The phone (`target: "mobile"`) follows the same declaration** — undeclared
|
|
446
|
+
drafts, declared sends. See `config/helps/custom-view-remote.md`.
|
|
447
447
|
|
|
448
448
|
Use this — not a hidden flag the user has to reconcile later — whenever a button
|
|
449
449
|
should _start backend work_: the user stays in the loop through trusted first-
|
|
@@ -152,6 +152,45 @@ checked**. Open the body with a line saying the answer came from this lookup and
|
|
|
152
152
|
is awaiting maintainer review — it is a draft, not documentation. Never edit
|
|
153
153
|
`bug-report-faq.md` yourself.
|
|
154
154
|
|
|
155
|
+
## "Failed to authenticate" — the Claude CLI's own login expired
|
|
156
|
+
|
|
157
|
+
### Symptoms
|
|
158
|
+
|
|
159
|
+
- `[Error] Failed to authenticate: OAuth session expired and could not be refreshed`
|
|
160
|
+
- `[Error] Failed to authenticate. API Error: 401 …` / `Invalid API key`
|
|
161
|
+
- Older versions: that text shown as a normal reply, then a separate `[Error] claude exited with code 1`
|
|
162
|
+
- Every turn fails the same way until the user acts — failures come in bursts, not at random
|
|
163
|
+
|
|
164
|
+
### Cause
|
|
165
|
+
|
|
166
|
+
The `claude` CLI that MulmoClaude spawns could not authenticate with Anthropic, so it
|
|
167
|
+
exited before running anything. This is the CLI's own login (`claude login`, stored in
|
|
168
|
+
`~/.claude` / the macOS Keychain), not a MulmoClaude or Google account link. An OAuth
|
|
169
|
+
(subscription) session goes stale when its silent refresh fails; nothing on the
|
|
170
|
+
MulmoClaude side can refresh it.
|
|
171
|
+
|
|
172
|
+
### Fix
|
|
173
|
+
|
|
174
|
+
The user runs `claude /login` (or `claude login`) in a terminal **on the host**, then
|
|
175
|
+
resends the message. If they authenticate with `ANTHROPIC_API_KEY` instead, check that
|
|
176
|
+
key. Switching auth mode is not the fix — a fresh `/login` is.
|
|
177
|
+
|
|
178
|
+
If `/login` succeeds and the error persists (typically `401 OAuth access token is
|
|
179
|
+
invalid`), what to check depends on the sandbox:
|
|
180
|
+
|
|
181
|
+
- **Sandbox off** — the spawned CLI inherits the server's environment, so a
|
|
182
|
+
`CLAUDE_CODE_OAUTH_TOKEN` set where MulmoClaude was started (shell profile or `.env`)
|
|
183
|
+
overrides the stored login. The user unsets or replaces it and **restarts
|
|
184
|
+
MulmoClaude**; a new terminal does not change the running server.
|
|
185
|
+
- **Sandbox on** — the container gets only the variables MulmoClaude passes explicitly,
|
|
186
|
+
and that token is not one of them; it authenticates from the host's mounted
|
|
187
|
+
`~/.claude`. Do not chase the environment variable here — the host login is the one
|
|
188
|
+
that counts.
|
|
189
|
+
|
|
190
|
+
You will usually be reading this AFTER the user re-logged in (a failing turn never
|
|
191
|
+
reaches you); answer "why did that happen" with the cause above rather than
|
|
192
|
+
investigating MulmoClaude's settings.
|
|
193
|
+
|
|
155
194
|
## gh / git / SSH errors inside the sandbox
|
|
156
195
|
|
|
157
196
|
### Symptoms
|
|
@@ -39,6 +39,13 @@ export interface CalendarCollectionPushResult {
|
|
|
39
39
|
deletedInGoogle: number;
|
|
40
40
|
/** Records that cannot be pushed as they stand, each with the reason. */
|
|
41
41
|
skipped: string[];
|
|
42
|
+
/** Deletions the sweep left standing in Google, each with the reason.
|
|
43
|
+
*
|
|
44
|
+
* NOT merged into `skipped`: that list means "this push did not do what you
|
|
45
|
+
* asked", and a refused deletion is "it was deliberately not deleted" while
|
|
46
|
+
* the rest of the run carried. Merging them made one refusal hide every
|
|
47
|
+
* successful create and update from the caller (#3272). */
|
|
48
|
+
keptInGoogle: string[];
|
|
42
49
|
errors: string[];
|
|
43
50
|
/** Records whose local edit did NOT reach Google and would be destroyed by
|
|
44
51
|
* the pull that follows an automatic push (#2620).
|
|
@@ -108,9 +115,28 @@ type PushOutcome = {
|
|
|
108
115
|
message: string;
|
|
109
116
|
};
|
|
110
117
|
export type PushOutcomeKind = PushOutcome["kind"];
|
|
118
|
+
/** One record's push, kept with the id so the pull can be told which records to
|
|
119
|
+
* leave alone. */
|
|
120
|
+
export interface PushAttempt {
|
|
121
|
+
eventId: string;
|
|
122
|
+
outcome: PushOutcome;
|
|
123
|
+
}
|
|
111
124
|
/** Whether this outcome leaves a local edit that only exists locally, so the
|
|
112
125
|
* pull must not overwrite the record and the baseline must not advance. */
|
|
113
126
|
export declare const isUnpushed: (kind: PushOutcomeKind) => boolean;
|
|
127
|
+
/** The whole report one run produces, from what each record's push did and what
|
|
128
|
+
* the delete sweep did.
|
|
129
|
+
*
|
|
130
|
+
* A sweep's refusal goes to `keptInGoogle`, NEVER to `skipped`: that list means
|
|
131
|
+
* "this push did not do what you asked", and a refusal means "the event was
|
|
132
|
+
* deliberately left standing" while the rest of the run carried. Merged, one
|
|
133
|
+
* refusal took the caller's problem branch and hid every create and update
|
|
134
|
+
* (#3272).
|
|
135
|
+
*
|
|
136
|
+
* Exported because it is pure — a list of attempts and a sweep, no workspace,
|
|
137
|
+
* grant or clock — so the separation above is something a test drives rather
|
|
138
|
+
* than argues. */
|
|
139
|
+
export declare function pushResultFrom(slug: string, attempts: readonly PushAttempt[], deletes: DeleteSweep): CalendarCollectionPushResult;
|
|
114
140
|
/** Record ids whose local value no longer matches the baseline — edits that have
|
|
115
141
|
* not reached Google.
|
|
116
142
|
*
|
|
@@ -133,6 +159,9 @@ export declare function unsentLocalEdits(collection: LoadedCollection, workspace
|
|
|
133
159
|
export interface DeleteSweep {
|
|
134
160
|
seen: number;
|
|
135
161
|
deleted: string[];
|
|
162
|
+
/** Deletions left standing in Google, each with the reason. Reported apart
|
|
163
|
+
* from the push's own `skipped` on the result: that list means "could not be
|
|
164
|
+
* sent", and a refusal means "deliberately left alone" (#3272). */
|
|
136
165
|
skipped: string[];
|
|
137
166
|
errors: string[];
|
|
138
167
|
}
|
package/dist/google/index.cjs
CHANGED
|
@@ -2034,9 +2034,22 @@ var UNPUSHED_KINDS = ["conflict", "error"];
|
|
|
2034
2034
|
/** Whether this outcome leaves a local edit that only exists locally, so the
|
|
2035
2035
|
* pull must not overwrite the record and the baseline must not advance. */
|
|
2036
2036
|
var isUnpushed = (kind) => UNPUSHED_KINDS.includes(kind);
|
|
2037
|
-
|
|
2037
|
+
/** The whole report one run produces, from what each record's push did and what
|
|
2038
|
+
* the delete sweep did.
|
|
2039
|
+
*
|
|
2040
|
+
* A sweep's refusal goes to `keptInGoogle`, NEVER to `skipped`: that list means
|
|
2041
|
+
* "this push did not do what you asked", and a refusal means "the event was
|
|
2042
|
+
* deliberately left standing" while the rest of the run carried. Merged, one
|
|
2043
|
+
* refusal took the caller's problem branch and hid every create and update
|
|
2044
|
+
* (#3272).
|
|
2045
|
+
*
|
|
2046
|
+
* Exported because it is pure — a list of attempts and a sweep, no workspace,
|
|
2047
|
+
* grant or clock — so the separation above is something a test drives rather
|
|
2048
|
+
* than argues. */
|
|
2049
|
+
function pushResultFrom(slug, attempts, deletes) {
|
|
2038
2050
|
const outcomes = attempts.map((attempt) => attempt.outcome);
|
|
2039
2051
|
const count = (kind) => outcomes.filter((outcome) => outcome.kind === kind).length;
|
|
2052
|
+
const messagesOf = (kind) => outcomes.flatMap((outcome) => outcome.kind === kind ? [outcome.message] : []);
|
|
2040
2053
|
return {
|
|
2041
2054
|
slug,
|
|
2042
2055
|
created: count("created"),
|
|
@@ -2044,8 +2057,9 @@ function tally(slug, attempts, deletes) {
|
|
|
2044
2057
|
conflicts: count("conflict"),
|
|
2045
2058
|
localDeletes: deletes.seen,
|
|
2046
2059
|
deletedInGoogle: deletes.deleted.length,
|
|
2047
|
-
skipped:
|
|
2048
|
-
|
|
2060
|
+
skipped: messagesOf("skipped"),
|
|
2061
|
+
keptInGoogle: [...deletes.skipped],
|
|
2062
|
+
errors: [...messagesOf("error"), ...deletes.errors],
|
|
2049
2063
|
unpushedIds: attempts.filter((attempt) => isUnpushed(attempt.outcome.kind)).map((attempt) => attempt.eventId)
|
|
2050
2064
|
};
|
|
2051
2065
|
}
|
|
@@ -2175,7 +2189,7 @@ async function pushCollectionNow(collection, workspaceRoot, deps = liveDeps) {
|
|
|
2175
2189
|
});
|
|
2176
2190
|
await saveCalendarShadow(calendarId, pushedShadow([outcome]), workspaceRoot);
|
|
2177
2191
|
}
|
|
2178
|
-
const result =
|
|
2192
|
+
const result = pushResultFrom(slug, attempts, await sweepDeletes(locallyDeletedIds(shadow, records.map((record) => fieldText(record[schema.primaryKey]))), schema.googleCalendar?.propagateDeletes === true, {
|
|
2179
2193
|
fetchEvent: (eventId) => (deps.fetchEvent ?? getCalendarEvent)(accessToken, {
|
|
2180
2194
|
calendarId,
|
|
2181
2195
|
eventId
|
|
@@ -2315,7 +2329,7 @@ function pullableEvents(events, unpushed) {
|
|
|
2315
2329
|
* A conflict or an error means that record now diverges from Google until
|
|
2316
2330
|
* someone resolves it, which must not be silent. */
|
|
2317
2331
|
function reportAutoPush(slug, result) {
|
|
2318
|
-
const { created, updated, conflicts, skipped, errors, unpushedIds } = result;
|
|
2332
|
+
const { created, updated, conflicts, skipped, keptInGoogle, errors, unpushedIds } = result;
|
|
2319
2333
|
if (created + updated > 0) log.info("google", "auto-pushed local calendar edits", {
|
|
2320
2334
|
slug,
|
|
2321
2335
|
created,
|
|
@@ -2330,6 +2344,10 @@ function reportAutoPush(slug, result) {
|
|
|
2330
2344
|
slug,
|
|
2331
2345
|
skipped
|
|
2332
2346
|
});
|
|
2347
|
+
if (keptInGoogle.length > 0) log.warn("google", "deletions the auto push left standing in Google", {
|
|
2348
|
+
slug,
|
|
2349
|
+
keptInGoogle
|
|
2350
|
+
});
|
|
2333
2351
|
if (errors.length > 0) log.warn("google", "auto push errors", {
|
|
2334
2352
|
slug,
|
|
2335
2353
|
errors
|
|
@@ -3264,6 +3282,7 @@ exports.pullableEvents = pullableEvents;
|
|
|
3264
3282
|
exports.pushAndProtect = pushAndProtect;
|
|
3265
3283
|
exports.pushCalendarForCollection = pushCalendarForCollection;
|
|
3266
3284
|
exports.pushCollectionNow = pushCollectionNow;
|
|
3285
|
+
exports.pushResultFrom = pushResultFrom;
|
|
3267
3286
|
exports.pushableMap = pushableMap;
|
|
3268
3287
|
exports.readDriveFile = readDriveFile;
|
|
3269
3288
|
exports.releaseOrphanedCalendarToken = releaseOrphanedCalendarToken;
|